diff --git a/CHANGELOG.md b/CHANGELOG.md index bd06d3d..596435a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,50 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [7.6.0] - 2026-05-07 + +- Android SDK version: 18.3.0 +- iOS SDK version: 6.14.4 + +### Breaking + +- `SuspiciousAppInfo.reason` (String) renamed to `reasons` (List\) +- Value `"blacklist"` in `reasons` renamed to `"blocklist"` + +### Flutter + +#### Deprecated + +- `blacklistedPackageNames`, `blacklistedHashes`, `suspiciousPermissions`, `whitelistedInstallationSources` are deprecated but remain functional — use `SuspiciousAppDetectionConfig` instead + +### Android + +#### Added + +- Added a new sub-check for `HMA` detection to the root detector +- Added a new sub-check for `KernelSU` detection to the root detector +- Added a new sub-check for `Frida Server` detection to the hook detector +- Added Huawei App Market provider to HMA detection queries +- New API class `SuspiciousAppDetectionConfig` that can be used to configure malware detection +- New API for malware detection configuration in `TalsecConfig`, see `TalsecConfig.Builder#suspiciousAppDetection` + +#### Fixed + +- Fixed `VerifyError` caused by `JaCoCo` bytecode instrumentation +- Fixed a potential cause of crash in the multi-instance detector +- Fixed crash caused by unhandled `SecurityException` thrown by `UsageStatsManager` in root detection +- Fixed manifest merge conflicts in HMA detection providers +- Fixed Java interoperability of `ScreenProtector` methods +- Fixed Kotlin classpath conflicts in SDK dependency resolution (Kotlin 2.0.0) + +#### Changed + +- Fine-tuned `KernelSU` detection +- Fine-tuned hook detection +- Fine-tuned location spoofing detection +- Modified malware incident log structure for better aggregation +- Old malware configuration API methods in `TalsecConfig.Builder` tagged as deprecated (but remain functional): `blacklistedPackageNames`, `blacklistedHashes`, `suspiciousPermissions`, `whitelistedInstallationSources` + ## [7.5.1] - 2026-03-24 - Android SDK version: 18.0.4 diff --git a/android/build.gradle b/android/build.gradle index 3802ef1..1193161 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -3,7 +3,7 @@ version '1.0-SNAPSHOT' buildscript { ext.kotlin_version = '2.1.0' - ext.talsec_version = '18.0.4' + ext.talsec_version = '18.3.0' repositories { google() mavenCentral() diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt b/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt index a5e9a1b..6b667a5 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt @@ -4,8 +4,13 @@ import android.content.Context import android.content.pm.PackageInfo import android.os.Build import com.aheaditec.talsec_security.security.api.ExternalIdResult +import com.aheaditec.talsec_security.security.api.MalwareScanScope +import com.aheaditec.talsec_security.security.api.ReasonMode +import com.aheaditec.talsec_security.security.api.ScopeType +import com.aheaditec.talsec_security.security.api.SuspiciousAppDetectionConfig import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import io.flutter.plugin.common.MethodChannel +import org.json.JSONObject import com.aheaditec.freerasp.generated.PackageInfo as FlutterPackageInfo import com.aheaditec.freerasp.generated.SuspiciousAppInfo as FlutterSuspiciousAppInfo @@ -33,7 +38,7 @@ internal inline fun runResultCatching(result: MethodChannel.Result, block: () -> * this [SuspiciousAppInfo]. */ internal fun SuspiciousAppInfo.toPigeon(context: Context): FlutterSuspiciousAppInfo { - return FlutterSuspiciousAppInfo(this.packageInfo.toPigeon(context), this.reason) + return FlutterSuspiciousAppInfo(this.packageInfo.toPigeon(context), this.reasons.toList()) } /** @@ -83,3 +88,48 @@ internal fun ExternalIdResult.resolve(result: MethodChannel.Result) { is ExternalIdResult.Error -> result.error("external-id-failure", this.errorMsg, null) } } + +internal fun JSONObject.toMalwareScanScope(): MalwareScanScope { + val scopeTypeStr = optString("scanScope", "SIDELOADED_ONLY") + val scanScope = runCatching { ScopeType.valueOf(scopeTypeStr) }.getOrDefault(ScopeType.SIDELOADED_ONLY) + val trustedInstallSources = optJSONArray("trustedInstallSources")?.let { arr -> + (0 until arr.length()).map { arr.getString(it) }.toSet() + } + return MalwareScanScope(scanScope = scanScope, trustedInstallSources = trustedInstallSources) +} + +internal fun JSONObject.toSuspiciousAppDetectionConfig(): SuspiciousAppDetectionConfig { + val packageNames = optJSONArray("packageNames")?.let { arr -> + (0 until arr.length()).map { arr.getString(it) }.toSet() + } + val hashes = optJSONArray("hashes")?.let { arr -> + (0 until arr.length()).map { arr.getString(it) }.toSet() + } + val requestedPermissions = optJSONArray("requestedPermissions")?.let { outer -> + (0 until outer.length()).map { i -> + val inner = outer.getJSONArray(i) + (0 until inner.length()).map { j -> inner.getString(j) }.toSet() + }.toSet() + } + val grantedPermissions = optJSONArray("grantedPermissions")?.let { outer -> + (0 until outer.length()).map { i -> + val inner = outer.getJSONArray(i) + (0 until inner.length()).map { j -> inner.getString(j) }.toSet() + }.toSet() + } + val malwareScanScope = optJSONObject("malwareScanScope")?.toMalwareScanScope() + val reasonModeStr = optString("reasonMode") + val reasonMode = if (reasonModeStr.isNullOrEmpty()) { + ReasonMode.HIGHEST_CONFIDENCE + } else { + runCatching { ReasonMode.valueOf(reasonModeStr) }.getOrDefault(ReasonMode.HIGHEST_CONFIDENCE) + } + return SuspiciousAppDetectionConfig( + packageNames = packageNames, + hashes = hashes, + requestedPermissions = requestedPermissions, + grantedPermissions = grantedPermissions, + malwareScanScope = malwareScanScope, + reasonMode = reasonMode, + ) +} diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt b/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt index 6b2c110..897e40f 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt @@ -38,7 +38,7 @@ internal object Utils { val alternativeStores = androidConfig.extractArray("supportedStores") val malwareConfig = parseMalwareConfig(androidConfig) - return TalsecConfig.Builder(packageName, certificateHashes) + val builder = TalsecConfig.Builder(packageName, certificateHashes) .watcherMail(watcherMail) .supportedAlternativeStores(alternativeStores) .prod(isProd) @@ -47,7 +47,12 @@ internal object Utils { .blacklistedHashes(malwareConfig.blacklistedHashes) .suspiciousPermissions(malwareConfig.suspiciousPermissions) .whitelistedInstallationSources(malwareConfig.whitelistedInstallationSources) - .build() + + androidConfig.optJSONObject("suspiciousAppDetectionConfig")?.let { + builder.suspiciousAppDetection(it.toSuspiciousAppDetectionConfig()) + } + + return builder.build() } private fun parseMalwareConfig(androidConfig: JSONObject): MalwareConfig { @@ -175,4 +180,4 @@ private inline fun processArray(jsonArray: JSONArray): Array { } return list.toTypedArray() -} \ No newline at end of file +} diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt b/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt index 05bae9b..3ae9472 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt @@ -62,20 +62,20 @@ data class PackageInfo ( /** Generated class from Pigeon that represents data sent in messages. */ data class SuspiciousAppInfo ( val packageInfo: PackageInfo, - val reason: String + val reasons: List ) { companion object { fun fromList(pigeonVar_list: List): SuspiciousAppInfo { val packageInfo = pigeonVar_list[0] as PackageInfo - val reason = pigeonVar_list[1] as String - return SuspiciousAppInfo(packageInfo, reason) + val reasons = pigeonVar_list[1] as List + return SuspiciousAppInfo(packageInfo, reasons) } } fun toList(): List { return listOf( packageInfo, - reason, + reasons, ) } } diff --git a/example/lib/main.dart b/example/lib/main.dart index 7a9311f..59ea3e1 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -38,12 +38,16 @@ Future _initializeTalsec() async { packageName: 'com.aheaditec.freeraspExample', signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], supportedStores: ['com.sec.android.app.samsungapps'], - malwareConfig: MalwareConfig( - blacklistedPackageNames: ['com.aheaditec.freeraspExample'], - suspiciousPermissions: [ + suspiciousAppDetectionConfig: const SuspiciousAppDetectionConfig( + packageNames: ['com.aheaditec.freeraspExample'], + requestedPermissions: [ ['android.permission.CAMERA'], ['android.permission.READ_SMS', 'android.permission.READ_CONTACTS'], ], + malwareScanScope: MalwareScanScope( + scanScope: ScopeType.sideloadedOnly, + ), + reasonMode: ReasonMode.highestConfidence, ), ), iosConfig: IOSConfig( diff --git a/example/lib/widgets/malware_bottom_sheet.dart b/example/lib/widgets/malware_bottom_sheet.dart index 04abf12..efcb27b 100644 --- a/example/lib/widgets/malware_bottom_sheet.dart +++ b/example/lib/widgets/malware_bottom_sheet.dart @@ -75,7 +75,7 @@ class MalwareListTile extends StatelessWidget { return ListTile( title: Text(malware.packageInfo.packageName), - subtitle: Text('Reason: ${malware.reason}'), + subtitle: Text('Reasons: ${malware.reasons.join(', ')}'), leading: appIcon, ); }, diff --git a/lib/src/generated/talsec_pigeon_api.g.dart b/lib/src/generated/talsec_pigeon_api.g.dart index fec170b..a6e3a50 100644 --- a/lib/src/generated/talsec_pigeon_api.g.dart +++ b/lib/src/generated/talsec_pigeon_api.g.dart @@ -63,17 +63,17 @@ class PackageInfo { class SuspiciousAppInfo { SuspiciousAppInfo({ required this.packageInfo, - required this.reason, + required this.reasons, }); PackageInfo packageInfo; - String reason; + List reasons; Object encode() { return [ packageInfo, - reason, + reasons, ]; } @@ -81,7 +81,7 @@ class SuspiciousAppInfo { result as List; return SuspiciousAppInfo( packageInfo: result[0]! as PackageInfo, - reason: result[1]! as String, + reasons: (result[1] as List?)!.cast(), ); } } diff --git a/lib/src/models/android_config.dart b/lib/src/models/android_config.dart index a613445..e4685f7 100644 --- a/lib/src/models/android_config.dart +++ b/lib/src/models/android_config.dart @@ -12,7 +12,8 @@ class AndroidConfig { required this.packageName, required this.signingCertHashes, this.supportedStores = const [], - this.malwareConfig, + @Deprecated('Use suspiciousAppDetectionConfig instead') this.malwareConfig, + this.suspiciousAppDetectionConfig, }) { ConfigVerifier.verifyAndroid(this); } @@ -34,5 +35,9 @@ class AndroidConfig { final List supportedStores; /// Malware configuration for Android. + @Deprecated('Use suspiciousAppDetectionConfig instead') final MalwareConfig? malwareConfig; + + /// Suspicious app detection configuration for Android. + final SuspiciousAppDetectionConfig? suspiciousAppDetectionConfig; } diff --git a/lib/src/models/android_config.g.dart b/lib/src/models/android_config.g.dart index ed4dc50..a0c415e 100644 --- a/lib/src/models/android_config.g.dart +++ b/lib/src/models/android_config.g.dart @@ -6,6 +6,7 @@ part of 'android_config.dart'; // JsonSerializableGenerator // ************************************************************************** +// ignore: deprecated_member_use_from_same_package AndroidConfig _$AndroidConfigFromJson(Map json) => AndroidConfig( packageName: json['packageName'] as String, @@ -20,6 +21,10 @@ AndroidConfig _$AndroidConfigFromJson(Map json) => ? null : MalwareConfig.fromJson( json['malwareConfig'] as Map), + suspiciousAppDetectionConfig: json['suspiciousAppDetectionConfig'] == null + ? null + : SuspiciousAppDetectionConfig.fromJson( + json['suspiciousAppDetectionConfig'] as Map), ); Map _$AndroidConfigToJson(AndroidConfig instance) { @@ -35,6 +40,9 @@ Map _$AndroidConfigToJson(AndroidConfig instance) { } } - writeNotNull('malwareConfig', instance.malwareConfig); + // ignore: deprecated_member_use_from_same_package + writeNotNull('malwareConfig', instance.malwareConfig?.toJson()); + writeNotNull('suspiciousAppDetectionConfig', + instance.suspiciousAppDetectionConfig?.toJson()); return val; } diff --git a/lib/src/models/malware_config.dart b/lib/src/models/malware_config.dart index 417d797..c520b5b 100644 --- a/lib/src/models/malware_config.dart +++ b/lib/src/models/malware_config.dart @@ -7,9 +7,13 @@ part 'malware_config.g.dart'; class MalwareConfig { /// Creates a new instance of [MalwareConfig]. MalwareConfig({ + @Deprecated('Use SuspiciousAppDetectionConfig instead') this.blacklistedPackageNames = const [], + @Deprecated('Use SuspiciousAppDetectionConfig instead') this.blacklistedHashes = const [], + @Deprecated('Use SuspiciousAppDetectionConfig instead') this.suspiciousPermissions = const [], + @Deprecated('Use SuspiciousAppDetectionConfig instead') this.whitelistedInstallationSources = const [], }); @@ -21,14 +25,18 @@ class MalwareConfig { Map toJson() => _$MalwareConfigToJson(this); /// List of blocklisted applications with given package name. + @Deprecated('Use SuspiciousAppDetectionConfig instead') final List blacklistedPackageNames; /// List of blocklisted applications with given hash. + @Deprecated('Use SuspiciousAppDetectionConfig instead') final List blacklistedHashes; /// List of blocklisted applications with given permissions. + @Deprecated('Use SuspiciousAppDetectionConfig instead') final List> suspiciousPermissions; /// List of whitelisted installation sources. + @Deprecated('Use SuspiciousAppDetectionConfig instead') final List whitelistedInstallationSources; } diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index 8b19213..7f5199a 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -1,4 +1,5 @@ export 'android_config.dart'; export 'ios_config.dart'; export 'malware_config.dart'; +export 'suspicious_app_detection_config.dart'; export 'talsec_config.dart'; diff --git a/lib/src/models/suspicious_app_detection_config.dart b/lib/src/models/suspicious_app_detection_config.dart new file mode 100644 index 0000000..351e881 --- /dev/null +++ b/lib/src/models/suspicious_app_detection_config.dart @@ -0,0 +1,92 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'suspicious_app_detection_config.g.dart'; + +/// The scope of apps to be scanned for malware. +enum ScopeType { + /// Only sideloaded apps. + sideloadedOnly, + + /// Sideloaded and system apps, excluding OEM apps. + sideloadedAndSystemExcludeOem, + + /// Sideloaded and OEM apps. + sideloadedAndOem, + + /// Sideloaded, system, and OEM apps. + sideloadedAndSystemAndOem, + + /// All apps. + all, +} + +/// The mode for reporting malware detection reasons. +enum ReasonMode { + /// Report all reasons. + all, + + /// Report only the highest confidence reason. + highestConfidence, +} + +/// Configuration for malware scan scope and trusted install sources. +@JsonSerializable(includeIfNull: false) +class MalwareScanScope { + /// Creates a new instance of [MalwareScanScope]. + const MalwareScanScope({ + required this.scanScope, + this.trustedInstallSources, + }); + + /// Converts from json + factory MalwareScanScope.fromJson(Map json) => + _$MalwareScanScopeFromJson(json); + + /// Converts to json + Map toJson() => _$MalwareScanScopeToJson(this); + + /// The scope of apps to be scanned. + final ScopeType scanScope; + + /// List of trusted install sources. + final List? trustedInstallSources; +} + +/// Configuration for suspicious app detection. +@JsonSerializable(includeIfNull: false) +class SuspiciousAppDetectionConfig { + /// Creates a new instance of [SuspiciousAppDetectionConfig]. + const SuspiciousAppDetectionConfig({ + this.packageNames, + this.hashes, + this.requestedPermissions, + this.grantedPermissions, + this.malwareScanScope, + this.reasonMode, + }); + + /// Converts from json + factory SuspiciousAppDetectionConfig.fromJson(Map json) => + _$SuspiciousAppDetectionConfigFromJson(json); + + /// Converts to json + Map toJson() => _$SuspiciousAppDetectionConfigToJson(this); + + /// List of suspicious package names to detect. + final List? packageNames; + + /// List of suspicious app hashes to detect. + final List? hashes; + + /// Sets of requested permissions that indicate a suspicious app. + final List>? requestedPermissions; + + /// Sets of granted permissions that indicate a suspicious app. + final List>? grantedPermissions; + + /// Configuration for the malware scan scope. + final MalwareScanScope? malwareScanScope; + + /// The mode for reporting detection reasons. + final ReasonMode? reasonMode; +} diff --git a/lib/src/models/suspicious_app_detection_config.g.dart b/lib/src/models/suspicious_app_detection_config.g.dart new file mode 100644 index 0000000..28f640a --- /dev/null +++ b/lib/src/models/suspicious_app_detection_config.g.dart @@ -0,0 +1,91 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'suspicious_app_detection_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +const _$ScopeTypeEnumMap = { + ScopeType.sideloadedOnly: 'SIDELOADED_ONLY', + ScopeType.sideloadedAndSystemExcludeOem: 'SIDELOADED_AND_SYSTEM_EXCLUDE_OEM', + ScopeType.sideloadedAndOem: 'SIDELOADED_AND_OEM', + ScopeType.sideloadedAndSystemAndOem: 'SIDELOADED_AND_SYSTEM_AND_OEM', + ScopeType.all: 'ALL', +}; + +const _$ReasonModeEnumMap = { + ReasonMode.all: 'ALL', + ReasonMode.highestConfidence: 'HIGHEST_CONFIDENCE', +}; + +MalwareScanScope _$MalwareScanScopeFromJson(Map json) => + MalwareScanScope( + scanScope: $enumDecode(_$ScopeTypeEnumMap, json['scanScope']), + trustedInstallSources: (json['trustedInstallSources'] as List?) + ?.map((e) => e as String) + .toList(), + ); + +Map _$MalwareScanScopeToJson(MalwareScanScope instance) { + final val = { + 'scanScope': _$ScopeTypeEnumMap[instance.scanScope]!, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('trustedInstallSources', instance.trustedInstallSources); + return val; +} + +SuspiciousAppDetectionConfig _$SuspiciousAppDetectionConfigFromJson( + Map json) => + SuspiciousAppDetectionConfig( + packageNames: (json['packageNames'] as List?) + ?.map((e) => e as String) + .toList(), + hashes: + (json['hashes'] as List?)?.map((e) => e as String).toList(), + requestedPermissions: (json['requestedPermissions'] as List?) + ?.map( + (e) => (e as List).map((e) => e as String).toList(), + ) + .toList(), + grantedPermissions: (json['grantedPermissions'] as List?) + ?.map( + (e) => (e as List).map((e) => e as String).toList(), + ) + .toList(), + malwareScanScope: json['malwareScanScope'] == null + ? null + : MalwareScanScope.fromJson( + json['malwareScanScope'] as Map), + reasonMode: $enumDecodeNullable(_$ReasonModeEnumMap, json['reasonMode']), + ); + +Map _$SuspiciousAppDetectionConfigToJson( + SuspiciousAppDetectionConfig instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('packageNames', instance.packageNames); + writeNotNull('hashes', instance.hashes); + writeNotNull('requestedPermissions', instance.requestedPermissions); + writeNotNull('grantedPermissions', instance.grantedPermissions); + writeNotNull('malwareScanScope', instance.malwareScanScope?.toJson()); + writeNotNull( + 'reasonMode', + instance.reasonMode == null + ? null + : _$ReasonModeEnumMap[instance.reasonMode!]); + return val; +} diff --git a/pigeons/talsec_pigeon_api.dart b/pigeons/talsec_pigeon_api.dart index 186abea..1c07b00 100644 --- a/pigeons/talsec_pigeon_api.dart +++ b/pigeons/talsec_pigeon_api.dart @@ -28,11 +28,11 @@ class PackageInfo { class SuspiciousAppInfo { const SuspiciousAppInfo({ required this.packageInfo, - required this.reason, + required this.reasons, }); final PackageInfo packageInfo; - final String reason; + final List reasons; } @FlutterApi() diff --git a/pubspec.yaml b/pubspec.yaml index 7c5716a..fd5562d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: freerasp description: Flutter library for improving app security and threat monitoring on Android and iOS mobile devices. Learn more about provided features on the freeRASP's homepage first. -version: 7.5.1 +version: 7.6.0 homepage: https://www.talsec.app/freerasp-in-app-protection-security-talsec repository: https://github.com/talsec/Free-RASP-Flutter