From 25960b1218d9c4890047eba49362ab55c55510c6 Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Thu, 3 Oct 2024 15:21:46 +0200 Subject: [PATCH 01/19] feat(ts): add malware detection --- android/build.gradle | 3 + .../FreeraspReactNativeModule.kt | 53 ++++++++- .../FreeraspThreatHandler.kt | 6 +- .../java/com/freeraspreactnative/Threat.kt | 4 +- .../java/com/freeraspreactnative/Utils.kt | 40 ------- .../models/RNSuspiciousAppInfo.kt | 25 ++++ .../freeraspreactnative/utils/Extensions.kt | 111 ++++++++++++++++++ .../com/freeraspreactnative/utils/Utils.kt | 79 +++++++++++++ 8 files changed, 275 insertions(+), 46 deletions(-) delete mode 100644 android/src/main/java/com/freeraspreactnative/Utils.kt create mode 100644 android/src/main/java/com/freeraspreactnative/models/RNSuspiciousAppInfo.kt create mode 100644 android/src/main/java/com/freeraspreactnative/utils/Extensions.kt create mode 100644 android/src/main/java/com/freeraspreactnative/utils/Utils.kt diff --git a/android/build.gradle b/android/build.gradle index 3a6febe..7c7284e 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -11,6 +11,7 @@ buildscript { classpath "com.android.tools.build:gradle:7.2.1" // noinspection DifferentKotlinGradleVersion classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" } } @@ -20,6 +21,7 @@ def isNewArchitectureEnabled() { apply plugin: "com.android.library" apply plugin: "kotlin-android" +apply plugin: 'kotlinx-serialization' if (isNewArchitectureEnabled()) { apply plugin: "com.facebook.react" @@ -87,6 +89,7 @@ dependencies { //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:$react_native_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" implementation "com.aheaditec.talsec.security:TalsecSecurity-Community-ReactNative:11.1.3" } diff --git a/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt b/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt index f75ed07..bba5fef 100644 --- a/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt +++ b/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt @@ -1,5 +1,6 @@ package com.freeraspreactnative +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import com.aheaditec.talsec_security.security.api.Talsec import com.aheaditec.talsec_security.security.api.TalsecConfig import com.aheaditec.talsec_security.security.api.ThreatListener @@ -12,8 +13,14 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.UiThreadUtil.runOnUiThread import com.facebook.react.bridge.WritableArray import com.facebook.react.modules.core.DeviceEventManagerModule - -class FreeraspReactNativeModule(val reactContext: ReactApplicationContext) : +import com.freeraspreactnative.utils.getArraySafe +import com.freeraspreactnative.utils.getBooleanSafe +import com.freeraspreactnative.utils.getMapThrowing +import com.freeraspreactnative.utils.getNestedArraySafe +import com.freeraspreactnative.utils.getStringThrowing +import com.freeraspreactnative.utils.toEncodedWritableArray + +class FreeraspReactNativeModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { private val listener = ThreatListener(FreeraspThreatHandler, FreeraspThreatHandler) @@ -42,8 +49,7 @@ class FreeraspReactNativeModule(val reactContext: ReactApplicationContext) : promise.resolve("freeRASP started") - } - catch (e: Exception) { + } catch (e: Exception) { promise.reject("TalsecInitializationError", e.message, e) } } @@ -65,6 +71,7 @@ class FreeraspReactNativeModule(val reactContext: ReactApplicationContext) : val channelData: WritableArray = Arguments.createArray() channelData.pushString(THREAT_CHANNEL_NAME) channelData.pushString(THREAT_CHANNEL_KEY) + channelData.pushString(MALWARE_CHANNEL_KEY) promise.resolve(channelData) } @@ -87,6 +94,15 @@ class FreeraspReactNativeModule(val reactContext: ReactApplicationContext) : // Remove upstream listeners, stop unnecessary background tasks } + /** + * Method to add apps to Malware whitelist, so they don't get flagged as malware + */ + @ReactMethod + fun addToWhitelist(packageName: String, promise: Promise) { + Talsec.addToWhitelist(reactContext, packageName) + promise.resolve(true) + } + private fun buildTalsecConfig(config: ReadableMap): TalsecConfig { val androidConfig = config.getMapThrowing("androidConfig") val packageName = androidConfig.getStringThrowing("packageName") @@ -97,6 +113,14 @@ class FreeraspReactNativeModule(val reactContext: ReactApplicationContext) : .supportedAlternativeStores(androidConfig.getArraySafe("supportedAlternativeStores")) .prod(config.getBooleanSafe("isProd")) + if (androidConfig.hasKey("malware")) { + val malwareConfig = androidConfig.getMapThrowing("malware") + talsecBuilder.whitelistedInstallationSources(malwareConfig.getArraySafe("whitelistedInstallationSources")) + talsecBuilder.blocklistedHashes(malwareConfig.getArraySafe("blocklistedHashes")) + talsecBuilder.blocklistedPermissions(malwareConfig.getNestedArraySafe("blocklistedPermissions")) + talsecBuilder.blocklistedPackageNames(malwareConfig.getArraySafe("blocklistedPackageNames")) + } + return talsecBuilder.build() } @@ -106,6 +130,8 @@ class FreeraspReactNativeModule(val reactContext: ReactApplicationContext) : .toString() // name of the channel over which threat callbacks are sent val THREAT_CHANNEL_KEY = (10000..999999999).random() .toString() // key of the argument map under which threats are expected + val MALWARE_CHANNEL_KEY = (10000..999999999).random() + .toString() // key of the argument map under which malware data is expected private lateinit var appReactContext: ReactApplicationContext private fun notifyListeners(threat: Threat) { val params = Arguments.createMap() @@ -114,11 +140,30 @@ class FreeraspReactNativeModule(val reactContext: ReactApplicationContext) : .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(THREAT_CHANNEL_NAME, params) } + + /** + * Sends malware detected event to React Native + */ + private fun notifyMalware(suspiciousApps: MutableList) { + val params = Arguments.createMap() + params.putInt(THREAT_CHANNEL_KEY, Threat.Malware.value) + params.putArray( + MALWARE_CHANNEL_KEY, suspiciousApps.toEncodedWritableArray(appReactContext) + ) + + appReactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit(THREAT_CHANNEL_NAME, params) + } } internal object ThreatListener : FreeraspThreatHandler.TalsecReactNative { override fun threatDetected(threatType: Threat) { notifyListeners(threatType) } + + override fun malwareDetected(suspiciousApps: MutableList) { + notifyMalware(suspiciousApps) + } } } diff --git a/android/src/main/java/com/freeraspreactnative/FreeraspThreatHandler.kt b/android/src/main/java/com/freeraspreactnative/FreeraspThreatHandler.kt index 4e58a44..d9facd8 100644 --- a/android/src/main/java/com/freeraspreactnative/FreeraspThreatHandler.kt +++ b/android/src/main/java/com/freeraspreactnative/FreeraspThreatHandler.kt @@ -39,7 +39,9 @@ internal object FreeraspThreatHandler : ThreatListener.ThreatDetected, ThreatLis listener?.threatDetected(Threat.ObfuscationIssues) } - override fun onMalwareDetected(p0: MutableList?) {} + override fun onMalwareDetected(suspiciousAppInfos: MutableList?) { + listener?.malwareDetected(suspiciousAppInfos ?: mutableListOf()) + } override fun onUnlockedDeviceDetected() { listener?.threatDetected(Threat.Passcode) @@ -59,5 +61,7 @@ internal object FreeraspThreatHandler : ThreatListener.ThreatDetected, ThreatLis internal interface TalsecReactNative { fun threatDetected(threatType: Threat) + + fun malwareDetected(suspiciousApps: MutableList) } } diff --git a/android/src/main/java/com/freeraspreactnative/Threat.kt b/android/src/main/java/com/freeraspreactnative/Threat.kt index ef53aab..d6fb7d0 100644 --- a/android/src/main/java/com/freeraspreactnative/Threat.kt +++ b/android/src/main/java/com/freeraspreactnative/Threat.kt @@ -23,6 +23,7 @@ internal sealed class Threat(val value: Int) { object ObfuscationIssues : Threat((10000..999999999).random()) object SystemVPN : Threat((10000..999999999).random()) object DevMode : Threat((10000..999999999).random()) + object Malware : Threat((10000..999999999).random()) companion object { internal fun getThreatValues(): WritableArray { @@ -39,7 +40,8 @@ internal sealed class Threat(val value: Int) { DeviceBinding.value, UnofficialStore.value, ObfuscationIssues.value, - DevMode.value + DevMode.value, + Malware.value ) ) } diff --git a/android/src/main/java/com/freeraspreactnative/Utils.kt b/android/src/main/java/com/freeraspreactnative/Utils.kt deleted file mode 100644 index 380dfd5..0000000 --- a/android/src/main/java/com/freeraspreactnative/Utils.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.freeraspreactnative - -import com.facebook.react.bridge.ReadableArray -import com.facebook.react.bridge.ReadableMap -import com.freeraspreactnative.exceptions.TalsecException - -internal fun ReadableMap.getMapThrowing(key: String): ReadableMap { - return this.getMap(key) ?: throw TalsecException("Key missing in configuration: $key") -} - -internal fun ReadableMap.getStringThrowing(key: String): String { - return this.getString(key) ?: throw TalsecException("Key missing in configuration: $key") -} - -internal fun ReadableMap.getBooleanSafe(key: String, defaultValue: Boolean = true): Boolean { - if (this.hasKey(key)) { - return this.getBoolean(key) - } - return defaultValue -} - -internal fun ReadableArray.toArray(): Array { - val output = mutableListOf() - for (i in 0 until this.size()) { - // in RN versions < 0.63, getString is nullable - @Suppress("UNNECESSARY_SAFE_CALL") - this.getString(i)?.let { - output.add(it) - } - } - return output.toTypedArray() -} - -internal fun ReadableMap.getArraySafe(key: String): Array { - if (this.hasKey(key)) { - val inputArray = this.getArray(key)!! - return inputArray.toArray() - } - return arrayOf() -} diff --git a/android/src/main/java/com/freeraspreactnative/models/RNSuspiciousAppInfo.kt b/android/src/main/java/com/freeraspreactnative/models/RNSuspiciousAppInfo.kt new file mode 100644 index 0000000..14f30d5 --- /dev/null +++ b/android/src/main/java/com/freeraspreactnative/models/RNSuspiciousAppInfo.kt @@ -0,0 +1,25 @@ +package com.freeraspreactnative.models + +import kotlinx.serialization.Serializable + + +/** + * Simplified, serializable wrapper for Talsec's SuspiciousAppInfo + */ +@Serializable +data class RNSuspiciousAppInfo( + val packageInfo: RNPackageInfo, + val reason: String, +) + +/** + * Simplified, serializable wrapper for Android's PackageInfo + */ +@Serializable +data class RNPackageInfo( + val packageName: String, + val appName: String?, + val version: String?, + val appIcon: String?, + val installerStore: String? +) diff --git a/android/src/main/java/com/freeraspreactnative/utils/Extensions.kt b/android/src/main/java/com/freeraspreactnative/utils/Extensions.kt new file mode 100644 index 0000000..d33714e --- /dev/null +++ b/android/src/main/java/com/freeraspreactnative/utils/Extensions.kt @@ -0,0 +1,111 @@ +package com.freeraspreactnative.utils + +import android.content.pm.PackageInfo +import android.util.Base64 +import android.util.Log +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableArray +import com.freeraspreactnative.exceptions.TalsecException +import com.freeraspreactnative.models.RNPackageInfo +import com.freeraspreactnative.models.RNSuspiciousAppInfo +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + + +internal fun ReadableMap.getMapThrowing(key: String): ReadableMap { + return this.getMap(key) ?: throw TalsecException("Key missing in configuration: $key") +} + +internal fun ReadableMap.getStringThrowing(key: String): String { + return this.getString(key) ?: throw TalsecException("Key missing in configuration: $key") +} + +internal fun ReadableMap.getBooleanSafe(key: String, defaultValue: Boolean = true): Boolean { + if (this.hasKey(key)) { + return this.getBoolean(key) + } + return defaultValue +} + +internal fun ReadableArray.toArray(): Array { + val output = mutableListOf() + for (i in 0 until this.size()) { + // in RN versions < 0.63, getString is nullable + @Suppress("UNNECESSARY_SAFE_CALL") + this.getString(i)?.let { + output.add(it) + } + } + return output.toTypedArray() +} + +internal fun ReadableMap.getArraySafe(key: String): Array { + if (this.hasKey(key)) { + val inputArray = this.getArray(key)!! + return inputArray.toArray() + } + return arrayOf() +} + +internal fun ReadableMap.getNestedArraySafe(key: String): Array> { + val outArray = mutableListOf>() + if (this.hasKey(key)) { + val inputArray = this.getArray(key)!! + for (i in 0 until inputArray.size()) { + outArray.add(inputArray.getArray(i).toArray()) + } + } + return outArray.toTypedArray() +} + + +/** + * Converts the Talsec's SuspiciousAppInfo to React Native equivalent + */ +internal fun SuspiciousAppInfo.toRNSuspiciousAppInfo(context: ReactContext): RNSuspiciousAppInfo { + return RNSuspiciousAppInfo( + packageInfo = this.packageInfo.toRNPackageInfo(context), + reason = this.reason, + ) +} + +/** + * Converts the Android's PackageInfo to React Native equivalent + */ +internal fun PackageInfo.toRNPackageInfo(context: ReactContext): RNPackageInfo { + return RNPackageInfo( + packageName = this.packageName, + appName = Utils.getAppName(context, this.applicationInfo), + version = this.versionName, + appIcon = Utils.getAppIconAsBase64String(context, this.packageName), + installerStore = Utils.getInstallationSource(context, this.packageName) + ) +} + +/** + * Convert the Talsec's SuspiciousAppInfo to base64-encoded json array, + * which can be then sent to React Native + */ +internal fun MutableList.toEncodedWritableArray(context: ReactContext): WritableArray { + val output = Arguments.createArray() + this.forEach { suspiciousAppInfo -> + val rnSuspiciousAppInfo = suspiciousAppInfo.toRNSuspiciousAppInfo(context) + try { + val encodedAppInfo = + Base64.encodeToString( + Json.encodeToString(rnSuspiciousAppInfo).toByteArray(), + Base64.DEFAULT + ) + output.pushString(encodedAppInfo) + } catch (e: Exception) { + Log.e("Talsec", "Could not serialize suspicious app data: ${e.message}") + } + + } + return output +} + diff --git a/android/src/main/java/com/freeraspreactnative/utils/Utils.kt b/android/src/main/java/com/freeraspreactnative/utils/Utils.kt new file mode 100644 index 0000000..9627ff9 --- /dev/null +++ b/android/src/main/java/com/freeraspreactnative/utils/Utils.kt @@ -0,0 +1,79 @@ +package com.freeraspreactnative.utils + +import android.content.pm.ApplicationInfo +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.os.Build +import android.util.Base64 +import com.facebook.react.bridge.ReactContext +import java.io.ByteArrayOutputStream + + +internal object Utils { + + private fun compressBitmap(bitmap: Bitmap): String { + val byteArrayOutputStream = ByteArrayOutputStream() + bitmap.compress(Bitmap.CompressFormat.PNG, 10, byteArrayOutputStream) + val byteArray = byteArrayOutputStream.toByteArray() + return Base64.encodeToString(byteArray, Base64.DEFAULT) + } + + /** + * Retrieves human-readable application name + */ + internal fun getAppName(context: ReactContext, applicationInfo: ApplicationInfo): String { + return context.packageManager.getApplicationLabel(applicationInfo) as String + } + + /** + * Retrieves app icon for the given package name as Drawable, transforms it to Bitmap, + * compresses to PNG and finally encodes the data to Base64 + * @param context React Native context + * @param packageName package name for which icon should be retrieved + * @return Base-64 encoded string + */ + internal fun getAppIconAsBase64String(context: ReactContext, packageName: String): String? { + try { + val drawable = context.packageManager.getApplicationIcon(packageName) + + if (drawable is BitmapDrawable && drawable.bitmap != null) { + return compressBitmap(drawable.bitmap) + } + + if (drawable.intrinsicWidth > 0 && drawable.intrinsicHeight > 0) { + val bitmap = Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return compressBitmap(bitmap) + } + return null + } catch (e: Exception) { + return null + } + } + + /** + * Retrieves installation source for the given package name + * @param context React Native context + * @param packageName package name for which installation source should be retrieved + * @return Installation source package name + */ + @Suppress("DEPRECATION") + internal fun getInstallationSource(context: ReactContext, packageName: String): String? { + return try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + context.packageManager.getInstallSourceInfo(packageName).installingPackageName + } else { + context.packageManager.getInstallerPackageName(packageName) + } + } catch (e: Exception) { + null + } + } +} From 5a21abd030cfc3ecaeff78d6bc602366cbb9986c Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Thu, 3 Oct 2024 15:24:55 +0200 Subject: [PATCH 02/19] feat(ts): add malware detection --- package.json | 3 ++- plugin/build/index.d.ts | 1 - src/definitions.ts | 46 +++++++++++++++++++++++++++++++++-------- src/index.tsx | 37 ++++++++++++++++++++++++++++++--- yarn.lock | 10 +++++++++ 5 files changed, 83 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 30cd986..9c17a67 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "freerasp-react-native", - "version": "3.9.3", + "version": "3.10.0", "description": "React Native plugin for improving app security and threat monitoring on Android and iOS mobile devices.", "main": "lib/commonjs/index", "module": "lib/module/index", @@ -12,6 +12,7 @@ "expo:typecheck": "cd plugin && tsc --noEmit", "lint": "eslint \"**/*.{js,ts,tsx}\"", "prepack": "bob build && yarn build:plugin", + "build": "bob build", "example": "yarn --cwd example", "bootstrap": "yarn example && yarn install && yarn example pods", "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build", diff --git a/plugin/build/index.d.ts b/plugin/build/index.d.ts index 45f689f..a03766e 100644 --- a/plugin/build/index.d.ts +++ b/plugin/build/index.d.ts @@ -1,4 +1,3 @@ -import { ConfigPlugin } from '@expo/config-plugins'; import { type PluginConfigType } from './pluginConfig'; declare const _default: ConfigPlugin; export default _default; diff --git a/src/definitions.ts b/src/definitions.ts index 13058fa..9de3d23 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -1,19 +1,44 @@ import { Platform } from 'react-native'; export type TalsecConfig = { - androidConfig?: { - packageName: string; - certificateHashes: string[]; - supportedAlternativeStores?: string[]; - }; - iosConfig?: { - appBundleId: string; - appTeamId: string; - }; + androidConfig?: TalsecAndroidConfig; + iosConfig?: TalsecIosConfig; watcherMail: string; isProd?: boolean; }; +export type TalsecAndroidConfig = { + packageName: string; + certificateHashes: string[]; + supportedAlternativeStores?: string[]; + malware?: TalsecMalwareConfig; +}; + +export type TalsecIosConfig = { + appBundleId: string; + appTeamId: string; +}; + +export type TalsecMalwareConfig = { + blocklistedHashes?: string[]; + blocklistedPackageNames?: string[]; + blocklistedPermissions?: string[][]; + whitelistedInstallationSources?: string[]; +}; + +export type SuspiciousAppInfo = { + packageInfo: PackageInfo; + reason: string; +}; + +export type PackageInfo = { + packageName: string; + appName?: string; + version?: string; + appIcon?: string; + installerStore?: string; +}; + export type NativeEventEmitterActions = { privilegedAccess?: () => any; debug?: () => any; @@ -28,6 +53,7 @@ export type NativeEventEmitterActions = { obfuscationIssues?: () => any; devMode?: () => any; systemVPN?: () => any; + malware?: (suspiciousApps: SuspiciousAppInfo[]) => any; }; export class Threat { @@ -46,6 +72,7 @@ export class Threat { static UnofficialStore = new Threat(0); static ObfuscationIssues = new Threat(0); static DevMode = new Threat(0); + static Malware = new Threat(0); constructor(value: number) { this.value = value; @@ -66,6 +93,7 @@ export class Threat { this.UnofficialStore, this.ObfuscationIssues, this.DevMode, + this.Malware, ] : [ this.AppIntegrity, diff --git a/src/index.tsx b/src/index.tsx index ec310d7..265dc18 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,14 +2,18 @@ import { useEffect } from 'react'; import { NativeEventEmitter, NativeModules, + Platform, type EmitterSubscription, } from 'react-native'; import { Threat, type NativeEventEmitterActions, + type PackageInfo, + type SuspiciousAppInfo, type TalsecConfig, } from './definitions'; import { getThreatCount, itemsHaveType } from './utils'; +import { decode } from 'base-64'; const { FreeraspReactNative } = NativeModules; @@ -31,9 +35,10 @@ const getThreatIdentifiers = async (): Promise => { return identifiers; }; -const getThreatChannelData = async (): Promise<[string, string]> => { +const getThreatChannelData = async (): Promise<[string, string, string]> => { + const dataLength = Platform.OS === 'ios' ? 2 : 3; let data = await FreeraspReactNative.getThreatChannelData(); - if (data.length !== 2 || !itemsHaveType(data, 'string')) { + if (data.length !== dataLength || !itemsHaveType(data, 'string')) { onInvalidCallback(); } return data; @@ -48,10 +53,25 @@ const prepareMapping = async (): Promise => { }); }; +// parses base64-encoded malware data to SuspiciousAppInfo[] +const parseMalwareData = (data: string[]): SuspiciousAppInfo[] => { + const result: SuspiciousAppInfo[] = []; + data.forEach((entry) => { + result.push(toSuspiciousAppInfo(entry)); + }); + return result; +}; + +const toSuspiciousAppInfo = (base64Value: string): SuspiciousAppInfo => { + const data = JSON.parse(decode(base64Value)); + const packageInfo = data.packageInfo as PackageInfo; + return { packageInfo, reason: data.reason } as SuspiciousAppInfo; +}; + export const setThreatListeners = async ( config: T & Record, []> ) => { - const [channel, key] = await getThreatChannelData(); + const [channel, key, malwareKey] = await getThreatChannelData(); await prepareMapping(); eventsListener = eventEmitter.addListener(channel, (event) => { @@ -98,6 +118,9 @@ export const setThreatListeners = async ( case Threat.SystemVPN.value: config.systemVPN?.(); break; + case Threat.Malware.value: + config.malware?.(parseMalwareData(event[malwareKey])); + break; default: onInvalidCallback(); break; @@ -138,4 +161,12 @@ export const useFreeRasp = ( }, []); }; +export const addToWhitelist = async (packageName: string): Promise => { + if (Platform.OS === 'ios') { + return Promise.reject('Malware detection not available on iOS'); + } + return FreeraspReactNative.addToWhitelist(packageName); +}; + +export * from './definitions'; export default FreeraspReactNative; diff --git a/yarn.lock b/yarn.lock index a4d5dc1..ec5ca8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2606,6 +2606,11 @@ dependencies: "@babel/types" "^7.20.7" +"@types/base-64@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/base-64/-/base-64-1.0.2.tgz#f7bc80d242306f20c57f076d79d1efe2d31032ca" + integrity sha512-uPgKMmM9fmn7I+Zi6YBqctOye4SlJsHKcisjHIMWpb2YKZRc36GpKyNuQ03JcT+oNXg1m7Uv4wU94EVltn8/cw== + "@types/graceful-fs@^4.1.3": version "4.1.9" resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz" @@ -3513,6 +3518,11 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base-64@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/base-64/-/base-64-1.0.0.tgz#09d0f2084e32a3fd08c2475b973788eee6ae8f4a" + integrity sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg== + base64-js@^1.1.2, base64-js@^1.2.3, base64-js@^1.3.1, base64-js@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" From 9ce7a424ddf70bb2d08b82fa66ad843d50f16e37 Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Thu, 3 Oct 2024 15:46:30 +0200 Subject: [PATCH 03/19] feat(example): add malware demo --- example/assets/arrow-down.png | Bin 0 -> 440 bytes example/assets/arrow-up.png | Bin 0 -> 427 bytes example/src/App.tsx | 54 +++++++++++- example/src/DemoApp.tsx | 18 +++- example/src/MalwareItem.tsx | 157 ++++++++++++++++++++++++++++++++++ example/src/MalwareModal.tsx | 126 +++++++++++++++++++++++++++ example/src/checks.ts | 1 + example/src/styles.ts | 1 + 8 files changed, 353 insertions(+), 4 deletions(-) create mode 100644 example/assets/arrow-down.png create mode 100644 example/assets/arrow-up.png create mode 100644 example/src/MalwareItem.tsx create mode 100644 example/src/MalwareModal.tsx diff --git a/example/assets/arrow-down.png b/example/assets/arrow-down.png new file mode 100644 index 0000000000000000000000000000000000000000..b24bb777910233af0866ba6a58da362897d27eea GIT binary patch literal 440 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDB3?!H8JlO)ISkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq8r{zDi(Vu$sZZAYL$MSD+10f(?({ zz5W0H|Aj4GPk=(qB|(0{4Er74e>b-Tav77n-CbDvGj0X~IqW5#zOL*qxR^OC)%L{P z{SOqH?&;zfVsZNI#X!Dh0}i%?CoH|Y52X1U?EHUM=WWDG{+qi`Rs{S!o!%jIckQ2OC7#SFu=o%X68W@KdSXh~wTN#^Z o8yHv_7_7f#`v65lZhlH;S|x4`_u5-yfEpM)UHx3vIVCg!03r92=>Px# literal 0 HcmV?d00001 diff --git a/example/assets/arrow-up.png b/example/assets/arrow-up.png new file mode 100644 index 0000000000000000000000000000000000000000..cd785b0c7d284f2f523a9c71ea2bf9358f53968b GIT binary patch literal 427 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDB3?!H8JlO)ISkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq8r{zDi(Vu$sZZAYL$MSD+10f(?({ zz5W0H|Ant7rUQkTOM?7@85$h6Ys!L5WlZvRcVX$zxEToKu$OrHy0X9EV&<@9YKXVg z0}6F{x;TbdoPIm$B5#8MhfBBcjXO;19CrQBliIlTX##`r&&A*8X&w!-i#D~KzU?^U zEH$TY(U?Xxrxg$QOaz%&{TFmPaJemL42W#p_`z#_W18fW;J6E#!rL{Bw`p*jn!YYy ze(r9{mVfUSFVFnVxYs}Yhif8Z27A(q57!bjGR_0(`Har|T-!c{2dJmy>WY;bAL|0T zO0~o_q9i4;B-JXpC>2OC7#SFu=o%X68W@KdSXh~wS(zGZ8yHv_7&u=@ypEzFH$Npa XtrE8equ!g@Kn)C@u6{1-oD!M { const [appChecks, setAppChecks] = React.useState([ ...commonChecks, ...(Platform.OS === 'ios' ? iosChecks : androidChecks), ]); + const [suspiciousApps, setSuspiciousApps] = React.useState< + SuspiciousAppInfo[] + >([]); + + useEffect(() => { + (async () => { + Platform.OS === 'android' && (await addItemsToMalwareWhitelist()); + })(); + }, []); const config = { androidConfig: { packageName: 'com.freeraspreactnativeexample', certificateHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], // supportedAlternativeStores: ['storeOne', 'storeTwo'], + malware: { + blocklistedHashes: ['FgvSehLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0u'], + blocklistedPackageNames: ['com.wultra.app.screenlogger'], + blocklistedPermissions: [ + ['android.permission.BLUETOOTH', 'android.permission.INTERNET'], + ['android.permission.INTERNET'], + ['android.permission.BATTERY_STATS'], + ], + whitelistedInstallationSources: ['com.apkpure.aegon'], + }, }, iosConfig: { appBundleId: 'com.freeraspreactnativeexample', @@ -144,11 +168,37 @@ const App = () => { ) ); }, + // Android only + malware: (detectedApps: SuspiciousAppInfo[]) => { + setSuspiciousApps(detectedApps); + setAppChecks((currentState) => + currentState.map((threat) => + threat.name === 'Malware' ? { ...threat, status: 'nok' } : threat + ) + ); + }, + }; + + const addItemsToMalwareWhitelist = async () => { + const appsToWhitelist = [ + 'com.talsecreactnativesecuritypluginexample', + 'com.example.myApp', + ]; + appsToWhitelist.forEach(async (app) => { + try { + const whitelistResponse = await addToWhitelist(app); + console.info( + `Malware Whitelist response for ${app}: ${whitelistResponse}` + ); + } catch (error: any) { + console.info('Error while adding app to malware whitelist: ', error); + } + }); }; useFreeRasp(config, actions); - return ; + return ; }; export default App; diff --git a/example/src/DemoApp.tsx b/example/src/DemoApp.tsx index f23a16b..8dd4578 100644 --- a/example/src/DemoApp.tsx +++ b/example/src/DemoApp.tsx @@ -6,8 +6,16 @@ import CloseCircle from '../assets/close-circle-outline.png'; import TalsecLogo from '../assets/talsec-logo.png'; import { Image } from 'react-native'; import { Colors } from './styles'; +import { MalwareModal } from './MalwareModal'; +import type { SuspiciousAppInfo } from 'freerasp-react-native'; -export const DemoApp = (props: any) => { +export const DemoApp: React.FC<{ + checks: { + name: string; + status: string; + }[]; + suspiciousApps: SuspiciousAppInfo[]; +}> = ({ checks, suspiciousApps }) => { return ( <> { > freeRASP checks: - {props.checks.map((check: any, idx: number) => ( + {checks.map((check: any, idx: number) => ( { > {check.name} + {check.name === 'Malware' && ( + + )} {check.status === 'ok' ? ( = ({ app }) => { + const [expanded, setExpanded] = useState(false); + + const appUninstall = async () => { + alert('Implement yourself!'); + }; + + const whitelistApp = async (packageName: string) => { + try { + const whitelistResponse = await addToWhitelist(packageName); + console.info( + `Malware Whitelist response for ${app}: ${whitelistResponse}` + ); + alert('Restart app for whitelist to take effect'); + } catch (error: any) { + console.info('Error while adding app to malware whitelist: ', error); + } + }; + + return ( + + setExpanded(!expanded)}> + + + + + {app.packageInfo.appName} + + + + + + + + + {expanded && ( + <> + + Package name: + {app.packageInfo.packageName} + App name: + + {app.packageInfo.appName ?? 'Not specified'} + + App version: + + {app.packageInfo.version ?? 'Not specified'} + + App Icon: + {app.packageInfo.appIcon ? ( + + ) : ( + Not specified + )} + Installer store: + + {app.packageInfo.installerStore ?? 'Not specified'} + + Detection reason: + {app.reason} + + + + + + )} + + ); +}; + +const styles = StyleSheet.create({ + button: { + borderRadius: 20, + paddingHorizontal: 30, + paddingVertical: 10, + marginTop: 15, + elevation: 2, + }, + buttonOpen: { + backgroundColor: '#F194FF', + }, + buttonClose: { + backgroundColor: '#2196F3', + }, + item: { + backgroundColor: '#d4e4ff', + borderRadius: 20, + padding: 20, + marginVertical: 8, + }, + listItemTitle: { + fontSize: 20, + fontWeight: 'bold', + }, + listItem: { + fontSize: 16, + paddingBottom: 5, + }, + titleText: { + fontSize: 20, + }, + icon: { + marginTop: 5, + marginBottom: 5, + width: 50, + height: 50, + }, + iconSmall: { + width: 40, + height: 40, + }, + textView: { + justifyContent: 'center', + flex: 1, + marginLeft: 20, + marginRight: 30, + }, + buttonView: { + justifyContent: 'center', + }, + spacer: { + height: 15, + }, + buttonGroup: { + marginTop: 10, + justifyContent: 'space-between', + }, +}); diff --git a/example/src/MalwareModal.tsx b/example/src/MalwareModal.tsx new file mode 100644 index 0000000..4af0d4d --- /dev/null +++ b/example/src/MalwareModal.tsx @@ -0,0 +1,126 @@ +import React, { useState } from 'react'; +import { + Alert, + FlatList, + Modal, + Pressable, + SafeAreaView, + StyleSheet, + Text, + View, + Button, +} from 'react-native'; +import type { SuspiciousAppInfo } from 'freerasp-react-native'; +import { MalwareItem } from './MalwareItem'; + +export const MalwareModal: React.FC<{ + isDisabled: boolean; + suspiciousApps: SuspiciousAppInfo[]; +}> = ({ isDisabled, suspiciousApps }) => { + const [modalVisible, setModalVisible] = useState(false); + + return ( + <> + {!isDisabled && ( + <> + + - + /> +