diff --git a/CHANGELOG.md b/CHANGELOG.md index bd06d3d..8c16f61 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). +## [8.0.0] - 2026-05-13 + +- 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"` +- Removed `MalwareConfig` and `AndroidConfig.malwareConfig` — use `SuspiciousAppDetectionConfig` instead + +### Flutter + +#### Removed + +- `MalwareConfig` class and `AndroidConfig.malwareConfig` field — 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 + ## [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..b05ad19 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt @@ -4,8 +4,14 @@ 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.JSONArray +import org.json.JSONObject import com.aheaditec.freerasp.generated.PackageInfo as FlutterPackageInfo import com.aheaditec.freerasp.generated.SuspiciousAppInfo as FlutterSuspiciousAppInfo @@ -33,7 +39,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 +89,31 @@ internal fun ExternalIdResult.resolve(result: MethodChannel.Result) { is ExternalIdResult.Error -> result.error("external-id-failure", this.errorMsg, null) } } + +internal fun JSONObject.toScanScope(): MalwareScanScope { + val scanScope = ScopeType.valueOf(getString("scanScope")) + val trustedInstallSources = optJSONArray("trustedInstallSources") + ?.let { processArray(it).asList() } + return MalwareScanScope(scanScope, trustedInstallSources) +} + +internal fun JSONObject.toSuspiciousAppDetectionConfig(): SuspiciousAppDetectionConfig { + val packageNames = optJSONArray("packageNames") + ?.let { processArray(it).toMutableSet() } + val hashes = optJSONArray("hashes") + ?.let { processArray(it).toMutableSet() } + val requestedPermissions = optJSONArray("requestedPermissions") + ?.let { processArray>(it).mapTo(mutableSetOf()) { it.toMutableSet() } } + val grantedPermissions = optJSONArray("grantedPermissions") + ?.let { processArray>(it).mapTo(mutableSetOf()) { it.toMutableSet() } } + val scanScope = getJSONObject("scanScope").toScanScope() + val reasonMode = ReasonMode.valueOf(getString("reasonMode")) + return SuspiciousAppDetectionConfig( + packageNames, + hashes, + requestedPermissions, + grantedPermissions, + scanScope, + 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..1a805a3 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt @@ -14,14 +14,6 @@ import org.json.JSONObject import java.io.ByteArrayOutputStream internal object Utils { - @Suppress("ArrayInDataClass") - data class MalwareConfig( - val blacklistedPackageNames: Array, - val blacklistedHashes: Array, - val suspiciousPermissions: Array>, - val whitelistedInstallationSources: Array - ) - fun toTalsecConfigThrowing(configJson: String?): TalsecConfig { if (configJson == null) { throw JSONException("Configuration is null") @@ -36,36 +28,20 @@ internal object Utils { val packageName = androidConfig.getString("packageName") val certificateHashes = androidConfig.extractArray("signingCertHashes") 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) .killOnBypass(killOnBypass) - .blacklistedPackageNames(malwareConfig.blacklistedPackageNames) - .blacklistedHashes(malwareConfig.blacklistedHashes) - .suspiciousPermissions(malwareConfig.suspiciousPermissions) - .whitelistedInstallationSources(malwareConfig.whitelistedInstallationSources) - .build() - } - private fun parseMalwareConfig(androidConfig: JSONObject): MalwareConfig { - if (!androidConfig.has("malwareConfig")) { - return MalwareConfig(emptyArray(), emptyArray(), emptyArray(), emptyArray()) + androidConfig.optJSONObject("suspiciousAppDetectionConfig")?.let { + builder.suspiciousAppDetection(it.toSuspiciousAppDetectionConfig()) } - val malwareConfig = androidConfig.getJSONObject("malwareConfig") - - return MalwareConfig( - malwareConfig.extractArray("blacklistedPackageNames"), - malwareConfig.extractArray("blacklistedHashes"), - malwareConfig.extractArray>("suspiciousPermissions"), - malwareConfig.extractArray("whitelistedInstallationSources") - ) + return builder.build() } - /** * Retrieves the package name of the installer for a given app package. * @@ -145,11 +121,11 @@ internal object Utils { } } -private inline fun JSONObject.extractArray(key: String): Array { +internal inline fun JSONObject.extractArray(key: String): Array { return this.optJSONArray(key)?.let { processArray(it) } ?: emptyArray() } -private inline fun processArray(jsonArray: JSONArray): Array { +internal inline fun processArray(jsonArray: JSONArray): Array { val list = mutableListOf() for (i in 0 until jsonArray.length()) { @@ -175,4 +151,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..138b4e5 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'], ], + scanScope: ScanScope( + 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..5543259 100644 --- a/lib/src/models/android_config.dart +++ b/lib/src/models/android_config.dart @@ -12,7 +12,7 @@ class AndroidConfig { required this.packageName, required this.signingCertHashes, this.supportedStores = const [], - this.malwareConfig, + this.suspiciousAppDetectionConfig, }) { ConfigVerifier.verifyAndroid(this); } @@ -33,6 +33,6 @@ class AndroidConfig { /// List of supported sources where application can be installed from. final List supportedStores; - /// Malware configuration for Android. - 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..43cb86f 100644 --- a/lib/src/models/android_config.g.dart +++ b/lib/src/models/android_config.g.dart @@ -16,10 +16,10 @@ AndroidConfig _$AndroidConfigFromJson(Map json) => ?.map((e) => e as String) .toList() ?? const [], - malwareConfig: json['malwareConfig'] == null + suspiciousAppDetectionConfig: json['suspiciousAppDetectionConfig'] == null ? null - : MalwareConfig.fromJson( - json['malwareConfig'] as Map), + : SuspiciousAppDetectionConfig.fromJson( + json['suspiciousAppDetectionConfig'] as Map), ); Map _$AndroidConfigToJson(AndroidConfig instance) { @@ -35,6 +35,7 @@ Map _$AndroidConfigToJson(AndroidConfig instance) { } } - writeNotNull('malwareConfig', instance.malwareConfig); + writeNotNull('suspiciousAppDetectionConfig', + instance.suspiciousAppDetectionConfig?.toJson()); return val; } diff --git a/lib/src/models/malware_config.dart b/lib/src/models/malware_config.dart deleted file mode 100644 index 417d797..0000000 --- a/lib/src/models/malware_config.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'malware_config.g.dart'; - -/// Configuration for malware detection. -@JsonSerializable() -class MalwareConfig { - /// Creates a new instance of [MalwareConfig]. - MalwareConfig({ - this.blacklistedPackageNames = const [], - this.blacklistedHashes = const [], - this.suspiciousPermissions = const [], - this.whitelistedInstallationSources = const [], - }); - - /// Converts config from json - factory MalwareConfig.fromJson(Map json) => - _$MalwareConfigFromJson(json); - - /// Converts config to json - Map toJson() => _$MalwareConfigToJson(this); - - /// List of blocklisted applications with given package name. - final List blacklistedPackageNames; - - /// List of blocklisted applications with given hash. - final List blacklistedHashes; - - /// List of blocklisted applications with given permissions. - final List> suspiciousPermissions; - - /// List of whitelisted installation sources. - final List whitelistedInstallationSources; -} diff --git a/lib/src/models/malware_config.g.dart b/lib/src/models/malware_config.g.dart deleted file mode 100644 index dbc31e4..0000000 --- a/lib/src/models/malware_config.g.dart +++ /dev/null @@ -1,38 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'malware_config.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -MalwareConfig _$MalwareConfigFromJson(Map json) => - MalwareConfig( - blacklistedPackageNames: - (json['blacklistedPackageNames'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], - blacklistedHashes: (json['blacklistedHashes'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], - suspiciousPermissions: (json['suspiciousPermissions'] as List?) - ?.map( - (e) => (e as List).map((e) => e as String).toList()) - .toList() ?? - const [], - whitelistedInstallationSources: - (json['whitelistedInstallationSources'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], - ); - -Map _$MalwareConfigToJson(MalwareConfig instance) => - { - 'blacklistedPackageNames': instance.blacklistedPackageNames, - 'blacklistedHashes': instance.blacklistedHashes, - 'suspiciousPermissions': instance.suspiciousPermissions, - 'whitelistedInstallationSources': instance.whitelistedInstallationSources, - }; diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index 8b19213..2db18eb 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -1,4 +1,4 @@ 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..c1c1106 --- /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 ScanScope { + /// Creates a new instance of [ScanScope]. + const ScanScope({ + required this.scanScope, + this.trustedInstallSources, + }); + + /// Converts from json + factory ScanScope.fromJson(Map json) => + _$ScanScopeFromJson(json); + + /// Converts to json + Map toJson() => _$ScanScopeToJson(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.scanScope = const ScanScope(scanScope: ScopeType.sideloadedOnly), + this.reasonMode = ReasonMode.highestConfidence, + }); + + /// 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 ScanScope scanScope; + + /// 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..73e68a3 --- /dev/null +++ b/lib/src/models/suspicious_app_detection_config.g.dart @@ -0,0 +1,87 @@ +// 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', +}; + +ScanScope _$ScanScopeFromJson(Map json) => ScanScope( + scanScope: $enumDecode(_$ScopeTypeEnumMap, json['scanScope']), + trustedInstallSources: (json['trustedInstallSources'] as List?) + ?.map((e) => e as String) + .toList(), + ); + +Map _$ScanScopeToJson(ScanScope 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(), + scanScope: json['scanScope'] == null + ? const ScanScope(scanScope: ScopeType.sideloadedOnly) + : ScanScope.fromJson(json['scanScope'] as Map), + reasonMode: + $enumDecodeNullable(_$ReasonModeEnumMap, json['reasonMode']) ?? + ReasonMode.highestConfidence, + ); + +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); + val['scanScope'] = instance.scanScope.toJson(); + val['reasonMode'] = _$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..3c119cb 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: 8.0.0 homepage: https://www.talsec.app/freerasp-in-app-protection-security-talsec repository: https://github.com/talsec/Free-RASP-Flutter diff --git a/test/src/models/android_config_test.dart b/test/src/models/android_config_test.dart index 34400bf..a90350f 100644 --- a/test/src/models/android_config_test.dart +++ b/test/src/models/android_config_test.dart @@ -126,5 +126,53 @@ void main() { throwsA(isA()), ); }); + + test('Should serialize suspiciousAppDetectionConfig to JSON', () { + final config = AndroidConfig( + packageName: 'com.example.app', + signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], + suspiciousAppDetectionConfig: const SuspiciousAppDetectionConfig( + packageNames: ['com.malware.app'], + scanScope: ScanScope(scanScope: ScopeType.all), + reasonMode: ReasonMode.all, + ), + ); + + final json = config.toJson(); + final detection = + json['suspiciousAppDetectionConfig'] as Map; + + expect(detection['packageNames'], ['com.malware.app']); + expect( + (detection['scanScope'] as Map)['scanScope'], + 'ALL', + ); + expect(detection['reasonMode'], 'ALL'); + }); + + test('Should omit suspiciousAppDetectionConfig from JSON when null', () { + final json = androidConfig.toJson(); + + expect(json.containsKey('suspiciousAppDetectionConfig'), isFalse); + }); + + test('Should deserialize suspiciousAppDetectionConfig from JSON', () { + final json = { + 'packageName': 'com.example.app', + 'signingCertHashes': ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], + 'suspiciousAppDetectionConfig': { + 'packageNames': ['com.malware.app'], + 'scanScope': {'scanScope': 'SIDELOADED_AND_OEM'}, + 'reasonMode': 'ALL', + }, + }; + + final config = AndroidConfig.fromJson(json); + final detection = config.suspiciousAppDetectionConfig!; + + expect(detection.packageNames, ['com.malware.app']); + expect(detection.scanScope.scanScope, ScopeType.sideloadedAndOem); + expect(detection.reasonMode, ReasonMode.all); + }); }); } diff --git a/test/src/models/suspicious_app_detection_config_test.dart b/test/src/models/suspicious_app_detection_config_test.dart new file mode 100644 index 0000000..f41ba67 --- /dev/null +++ b/test/src/models/suspicious_app_detection_config_test.dart @@ -0,0 +1,262 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:freerasp/freerasp.dart'; + +void main() { + group('ScanScope', () { + test('Should create ScanScope instance with required field only', () { + const scope = ScanScope(scanScope: ScopeType.sideloadedOnly); + + expect(scope, isA()); + expect(scope.scanScope, ScopeType.sideloadedOnly); + expect(scope.trustedInstallSources, isNull); + }); + + test('Should create ScanScope instance with all fields', () { + const scope = ScanScope( + scanScope: ScopeType.all, + trustedInstallSources: ['com.android.vending'], + ); + + expect(scope.scanScope, ScopeType.all); + expect(scope.trustedInstallSources, ['com.android.vending']); + }); + + test('Should convert ScanScope to JSON', () { + const scope = ScanScope( + scanScope: ScopeType.sideloadedAndOem, + trustedInstallSources: ['com.android.vending'], + ); + + final json = scope.toJson(); + + expect(json['scanScope'], 'SIDELOADED_AND_OEM'); + expect(json['trustedInstallSources'], ['com.android.vending']); + }); + + test('Should omit trustedInstallSources from JSON when null', () { + const scope = ScanScope(scanScope: ScopeType.sideloadedOnly); + + final json = scope.toJson(); + + expect(json.containsKey('trustedInstallSources'), isFalse); + }); + + test('Should create ScanScope instance from JSON', () { + final json = { + 'scanScope': 'SIDELOADED_AND_SYSTEM_EXCLUDE_OEM', + 'trustedInstallSources': ['com.android.vending'], + }; + + final scope = ScanScope.fromJson(json); + + expect(scope.scanScope, ScopeType.sideloadedAndSystemExcludeOem); + expect(scope.trustedInstallSources, ['com.android.vending']); + }); + + test('Should create ScanScope instance from JSON without optional', () { + final json = {'scanScope': 'ALL'}; + + final scope = ScanScope.fromJson(json); + + expect(scope.scanScope, ScopeType.all); + expect(scope.trustedInstallSources, isNull); + }); + }); + + group('SuspiciousAppDetectionConfig', () { + test('Should create instance with defaults when no args provided', () { + const config = SuspiciousAppDetectionConfig(); + + expect(config.packageNames, isNull); + expect(config.hashes, isNull); + expect(config.requestedPermissions, isNull); + expect(config.grantedPermissions, isNull); + expect(config.scanScope.scanScope, ScopeType.sideloadedOnly); + expect(config.scanScope.trustedInstallSources, isNull); + expect(config.reasonMode, ReasonMode.highestConfidence); + }); + + test('Should create instance with all fields populated', () { + const config = SuspiciousAppDetectionConfig( + packageNames: ['com.malware.app'], + hashes: ['abc123'], + requestedPermissions: [ + ['android.permission.CAMERA'], + ], + grantedPermissions: [ + ['android.permission.READ_SMS'], + ], + scanScope: ScanScope(scanScope: ScopeType.all), + reasonMode: ReasonMode.all, + ); + + expect(config.packageNames, ['com.malware.app']); + expect(config.hashes, ['abc123']); + expect(config.requestedPermissions, [ + ['android.permission.CAMERA'], + ]); + expect(config.grantedPermissions, [ + ['android.permission.READ_SMS'], + ]); + expect(config.scanScope.scanScope, ScopeType.all); + expect(config.reasonMode, ReasonMode.all); + }); + + test('Should always serialize defaults to JSON', () { + const config = SuspiciousAppDetectionConfig(); + + final json = config.toJson(); + + expect(json['scanScope'], isA>()); + expect( + (json['scanScope'] as Map)['scanScope'], + 'SIDELOADED_ONLY', + ); + expect(json['reasonMode'], 'HIGHEST_CONFIDENCE'); + expect(json.containsKey('packageNames'), isFalse); + expect(json.containsKey('hashes'), isFalse); + expect(json.containsKey('requestedPermissions'), isFalse); + expect(json.containsKey('grantedPermissions'), isFalse); + }); + + test('Should convert SuspiciousAppDetectionConfig to JSON', () { + const config = SuspiciousAppDetectionConfig( + packageNames: ['com.malware.app'], + hashes: ['abc123'], + requestedPermissions: [ + ['android.permission.CAMERA'], + ], + grantedPermissions: [ + ['android.permission.READ_SMS', 'android.permission.READ_CONTACTS'], + ], + scanScope: ScanScope( + scanScope: ScopeType.sideloadedAndSystemAndOem, + trustedInstallSources: ['com.android.vending'], + ), + reasonMode: ReasonMode.all, + ); + + final json = config.toJson(); + + expect(json['packageNames'], ['com.malware.app']); + expect(json['hashes'], ['abc123']); + expect(json['requestedPermissions'], [ + ['android.permission.CAMERA'], + ]); + expect(json['grantedPermissions'], [ + ['android.permission.READ_SMS', 'android.permission.READ_CONTACTS'], + ]); + expect( + (json['scanScope'] as Map)['scanScope'], + 'SIDELOADED_AND_SYSTEM_AND_OEM', + ); + expect(json['reasonMode'], 'ALL'); + }); + + test('Should create from JSON applying defaults for missing fields', () { + final config = SuspiciousAppDetectionConfig.fromJson({}); + + expect(config.packageNames, isNull); + expect(config.scanScope.scanScope, ScopeType.sideloadedOnly); + expect(config.reasonMode, ReasonMode.highestConfidence); + }); + + test('Should create SuspiciousAppDetectionConfig from JSON', () { + final json = { + 'packageNames': ['com.malware.app'], + 'hashes': ['abc123'], + 'requestedPermissions': [ + ['android.permission.CAMERA'], + ], + 'grantedPermissions': [ + ['android.permission.READ_SMS'], + ], + 'scanScope': { + 'scanScope': 'SIDELOADED_AND_OEM', + 'trustedInstallSources': ['com.android.vending'], + }, + 'reasonMode': 'ALL', + }; + + final config = SuspiciousAppDetectionConfig.fromJson(json); + + expect(config.packageNames, ['com.malware.app']); + expect(config.hashes, ['abc123']); + expect(config.requestedPermissions, [ + ['android.permission.CAMERA'], + ]); + expect(config.grantedPermissions, [ + ['android.permission.READ_SMS'], + ]); + expect(config.scanScope.scanScope, ScopeType.sideloadedAndOem); + expect( + config.scanScope.trustedInstallSources, + ['com.android.vending'], + ); + expect(config.reasonMode, ReasonMode.all); + }); + + test('Should round-trip through JSON without loss', () { + const original = SuspiciousAppDetectionConfig( + packageNames: ['com.malware.app'], + hashes: ['abc123'], + scanScope: ScanScope( + scanScope: ScopeType.sideloadedAndSystemExcludeOem, + ), + reasonMode: ReasonMode.all, + ); + + final restored = SuspiciousAppDetectionConfig.fromJson(original.toJson()); + + expect(restored.packageNames, original.packageNames); + expect(restored.hashes, original.hashes); + expect( + restored.scanScope.scanScope, + original.scanScope.scanScope, + ); + expect(restored.reasonMode, original.reasonMode); + }); + }); + + group('ScopeType', () { + test('Should map every value to its wire format string', () { + const expected = { + 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', + }; + + for (final entry in expected.entries) { + final json = ScanScope(scanScope: entry.key).toJson(); + expect(json['scanScope'], entry.value); + + final restored = ScanScope.fromJson({'scanScope': entry.value}); + expect(restored.scanScope, entry.key); + } + }); + }); + + group('ReasonMode', () { + test('Should map every value to its wire format string', () { + const expected = { + ReasonMode.all: 'ALL', + ReasonMode.highestConfidence: 'HIGHEST_CONFIDENCE', + }; + + for (final entry in expected.entries) { + final json = + SuspiciousAppDetectionConfig(reasonMode: entry.key).toJson(); + expect(json['reasonMode'], entry.value); + + final restored = SuspiciousAppDetectionConfig.fromJson({ + 'scanScope': {'scanScope': 'SIDELOADED_ONLY'}, + 'reasonMode': entry.value, + }); + expect(restored.reasonMode, entry.key); + } + }); + }); +}