From 1668bb450236966c29f901267e01498c3d4f021e Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Tue, 10 Nov 2026 15:35:18 +0100 Subject: [PATCH 1/9] release: freeRASP 5.4.0 (Android SDK 20.0.1) Bump the Android SDK to 20.0.1 (app.talsec.rasp:TalsecRasp-Community-ReactNative) and expose its new features: - ongoing call detection: ongoingCall threat action with CallState, startOngoingCallDetection/stopOngoingCallDetection - screen control blocking: blockScreenControl/isScreenControlBlocked - overlay windows hiding: blockOverlayWindows/isOverlayWindowsBlocked - criticalChecksFinished/postponedChecksFinished execution states The new threat and execution-state identifiers and the call-state channel key are Android only; the TypeScript lists are branched per platform so the iOS identifier counts stay unchanged. allChecksFinished is now driven by the SDK's onFastChecksFinished, which fires where the removed onAllChecksFinished used to. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 58 +++++++ README.md | 2 + android/build.gradle | 2 +- .../FreeraspReactNativeModule.kt | 164 +++++++++++++++++- .../PluginThreatHandler.kt | 17 +- .../dispatchers/ThreatDispatcher.kt | 20 +++ .../events/RaspExecutionStateEvent.kt | 6 +- .../freeraspreactnative/events/ThreatEvent.kt | 5 +- .../interfaces/PluginThreatListener.kt | 2 + .../android/app/src/main/AndroidManifest.xml | 4 + example/src/App.tsx | 27 ++- example/src/DemoApp.tsx | 146 ++++++++++++---- example/src/checks.ts | 1 + package.json | 2 +- src/api/listeners/raspExecutionState.ts | 6 + src/api/listeners/threat.ts | 25 ++- src/api/methods/native.ts | 68 ++++++++ src/channels/threat.ts | 7 +- src/models/raspExecutionState.ts | 12 +- src/models/threat.ts | 2 + src/types/types.ts | 31 +++- 21 files changed, 556 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f891bc..b603f93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,64 @@ 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). +## [5.4.0] - 2026-11-10 + +- Android SDK version: 20.0.1 +- iOS SDK version: 8.0.1 + +### React Native + +#### Added + +- `ongoingCall` threat action with the new `CallState` type (`'STARTED' | 'ONGOING' | 'STOPPED'`), driven by `startOngoingCallDetection()` and `stopOngoingCallDetection()` (Android only) +- `blockScreenControl()` and `isScreenControlBlocked()` to prevent accessibility services from reading or controlling the app screen (Android only) +- `blockOverlayWindows()` and `isOverlayWindowsBlocked()` to hide overlay windows of other apps (Android 12+ only, requires the `HIDE_OVERLAY_WINDOWS` permission) +- `criticalChecksFinished` and `postponedChecksFinished` RASP execution state actions (Android only) + +#### Changed + +- On Android, `allChecksFinished` is now triggered when the fast checks are completed, where it was triggered before; the postponed checks may still run afterwards +- On Android, `isScreenCaptureBlocked()` reports the protection state of the current activity + +### Android + +#### Breaking Changes + +- ❗️Breaking: `ThreatListener.RaspExecutionState.onAllChecksFinished()` was removed and replaced by two callbacks: + - `onFastChecksFinished()` is triggered after the fast standard checks are completed (where `onAllChecksFinished()` used to be triggered) + - `onPostponedChecksFinished()` is triggered after the postponed/deep checks are completed +- ❗️Breaking: Modified method `Talsec.isScreenCaptureBlocked(activity: Activity): Boolean` which now takes activity as parameter, and returns if the given activity view is protected against screen capture +- ❗️Breaking: Renamed Maven coordinates from `com.aheaditec.talsec.security:TalsecSecurity-{flavor}` + +#### Added + +- Added `isScreenControlBlocked(activity: Activity): Boolean` to check if the screen control protection is set for given activity +- Added `blockScreenControl(activity: Activity, enable: Boolean)` to prevent accessibility services from reading or controlling an activity's screen +- Added `blockOverlayWindows(activity: Activity, enable: Boolean): Boolean` to hide overlay windows of other apps over the given activity (Android 12+, requires `HIDE_OVERLAY_WINDOWS` permission) +- Added `isOverlayWindowsBlocked(activity: Activity): Boolean` to check if overlay windows are hidden for given activity +- Added a new `Ongoing Call` detection engine (VoIP, Cellular) + - started/paused per activity with `startOngoingCallDetection` / `stopOngoingCallDetection` + - new callback `onOngoingCall(CallState)` in `ThreatListener.ThreatDetected` + - new `CallState` enum with 3 call states (`STARTED`) +- Added callback `onCriticalChecksFinished()` in `ThreatListener.RaspExecutionState`, which is triggered after critical checks are completed + +#### Fixed + +- Improved hook detection to prevent collisions with running VPN services and to better work with the device resources (threads/RAM) +- Fixed a release crash during root detection when a checked package is not installed +- Fixed a crash in `ScreenProtector.unregisterScreenCallbacks` on Android 15+ when several activities registered screen callbacks +- Fixed intermittent build failures of apps using the SDK (`NullPointerException: Cannot read field "d"`, shown by Gradle as `[CIRCULAR REFERENCE]`) caused by invalid bytecode in some SDK builds +- Fixed `killOnBypass` closing the app when the same threat was reported twice in quick succession + +#### Changed + +- Improved Bootloader detection +- Extended emulator detection with additional device-property and ABI checks +- Improved logging structure and data enrichment for better Portal insights/reports +- Refactoring of the time spoofing detection which improves the performance of the freeRASP check execution +- Improved the performance of the App integrity check +- Updated CA bundle + ## [5.3.0] - 2026-11-09 - Android SDK version: 19.3.1 diff --git a/README.md b/README.md index 1d2db69..15296b8 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ Key features are the detection and prevention of - Unlocked or compromised bootloader - Hooking framework (e.g., Frida, LSPosed, Shadow) - UI automation and driven-input attacks (e.g., Appium) +- Ongoing cellular and VoIP calls during sensitive flows (Android) +- Screen control by accessibility services and overlay windows of other apps (Android) - Untrusted installation method - App Data Migration protection diff --git a/android/build.gradle b/android/build.gradle index 0c1c820..bed0da1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -57,7 +57,7 @@ android { compileSdk getMaxVersion("compileSdk") talsec { - sdkVersion = "19.3.1" + sdkVersion = "20.0.1" platform = "ReactNative" } diff --git a/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt b/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt index dc71092..a1163ff 100644 --- a/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt +++ b/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt @@ -1,5 +1,7 @@ package com.freeraspreactnative +import android.annotation.SuppressLint +import android.app.Activity import android.os.Build import android.os.Handler import android.os.HandlerThread @@ -9,6 +11,7 @@ import app.talsec.rasp.security.api.SuspiciousAppInfo import app.talsec.rasp.security.api.Talsec import app.talsec.rasp.security.api.TalsecConfig import app.talsec.rasp.security.api.TalsecMode +import app.talsec.rasp.security.api.ThreatListener.CallState import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise @@ -34,6 +37,7 @@ import com.freeraspreactnative.utils.getNestedArraySafe import com.freeraspreactnative.utils.getStringThrowing import com.freeraspreactnative.utils.toEncodedWritableArray import com.freeraspreactnative.utils.toSuspiciousAppDetectionConfig +import java.lang.ref.WeakReference class FreeraspReactNativeModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { @@ -54,6 +58,7 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex reactContext.currentActivity?.let { ScreenProtector.unregister(it) } } if (reactContext.currentActivity?.isFinishing == true) { + stopOngoingCallDetectionInternal() ThreatDispatcher.unregisterListener() ExecutionStateDispatcher.unregisterListener() PluginThreatHandler.unregisterSDKListener(reactContext) @@ -134,7 +139,7 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex /** * Method to setup the threat message passing between native and React Native - * @return list of [THREAT_CHANNEL_NAME, THREAT_CHANNEL_KEY] + * @return list of [THREAT_CHANNEL_NAME, THREAT_CHANNEL_KEY, MALWARE_CHANNEL_KEY, CALL_STATE_CHANNEL_KEY] */ @ReactMethod fun getThreatChannelData(promise: Promise) { @@ -142,6 +147,7 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex channelData.pushString(ThreatEvent.CHANNEL_NAME) channelData.pushString(ThreatEvent.CHANNEL_KEY) channelData.pushString(ThreatEvent.MALWARE_CHANNEL_KEY) + channelData.pushString(ThreatEvent.CALL_STATE_CHANNEL_KEY) promise.resolve(channelData) } @@ -245,14 +251,158 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex */ @ReactMethod fun isScreenCaptureBlocked(promise: Promise) { + val activity = reactContext.currentActivity ?: run { + promise.reject( + "NativePluginError", "Cannot check screen capture state, activity is null." + ) + return + } + try { - val isBlocked = Talsec.isScreenCaptureBlocked() + val isBlocked = Talsec.isScreenCaptureBlocked(activity) promise.resolve(isBlocked) } catch (e: Exception) { promise.reject("NativePluginError", "Error in isScreenCaptureBlocked: ${e.message}") } } + /** + * Method Block/Unblock accessibility services from reading or controlling the screen + * @param enable boolean for whether want to block or unblock the screen control + */ + @ReactMethod + fun blockScreenControl(enable: Boolean, promise: Promise) { + val activity = reactContext.currentActivity ?: run { + promise.reject( + "NativePluginError", "Cannot block screen control, activity is null." + ) + return + } + + runOnUiThread { + try { + Talsec.blockScreenControl(activity, enable) + promise.resolve("Screen control is now ${if (enable) "Blocked" else "Enabled"}.") + } catch (e: Exception) { + promise.reject("NativePluginError", "Error in blockScreenControl: ${e.message}") + } + } + } + + /** + * Method Returns whether screen control is blocked or not + * @return boolean for is screen control blocked or not + */ + @ReactMethod + fun isScreenControlBlocked(promise: Promise) { + val activity = reactContext.currentActivity ?: run { + promise.reject( + "NativePluginError", "Cannot check screen control state, activity is null." + ) + return + } + + try { + promise.resolve(Talsec.isScreenControlBlocked(activity)) + } catch (e: Exception) { + promise.reject("NativePluginError", "Error in isScreenControlBlocked: ${e.message}") + } + } + + /** + * Method Hide/Show overlay windows of other apps over the activity. + * Requires Android 12+ and the HIDE_OVERLAY_WINDOWS permission. + * @param enable boolean for whether want to hide or show overlay windows + * @return boolean whether the request was applied + */ + // Missing permission is suppressed because the decision to use the overlay API is made + // by developer, and not enforced by the library. + @SuppressLint("MissingPermission") + @ReactMethod + fun blockOverlayWindows(enable: Boolean, promise: Promise) { + val activity = reactContext.currentActivity ?: run { + promise.reject( + "NativePluginError", "Cannot block overlay windows, activity is null." + ) + return + } + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + promise.resolve(false) + return + } + + runOnUiThread { + try { + promise.resolve(Talsec.blockOverlayWindows(activity, enable)) + } catch (e: Exception) { + promise.reject("NativePluginError", "Error in blockOverlayWindows: ${e.message}") + } + } + } + + /** + * Method Returns whether overlay windows of other apps are hidden or not + * @return boolean for are overlay windows hidden or not + */ + @ReactMethod + fun isOverlayWindowsBlocked(promise: Promise) { + val activity = reactContext.currentActivity ?: run { + promise.reject( + "NativePluginError", "Cannot check overlay windows state, activity is null." + ) + return + } + + try { + promise.resolve(Talsec.isOverlayWindowsBlocked(activity)) + } catch (e: Exception) { + promise.reject("NativePluginError", "Error in isOverlayWindowsBlocked: ${e.message}") + } + } + + /** + * Method starts the ongoing call (cellular and VoIP) detection for the current activity + */ + @ReactMethod + fun startOngoingCallDetection(promise: Promise) { + val activity = reactContext.currentActivity ?: run { + promise.reject( + "NativePluginError", "Cannot start ongoing call detection, activity is null." + ) + return + } + + try { + if (ongoingCallActivity?.get() !== activity) { + stopOngoingCallDetectionInternal() + Talsec.startOngoingCallDetection(activity) + ongoingCallActivity = WeakReference(activity) + } + promise.resolve("Ongoing call detection started.") + } catch (e: Exception) { + promise.reject("NativePluginError", "Error in startOngoingCallDetection: ${e.message}") + } + } + + /** + * Method stops the ongoing call detection + */ + @ReactMethod + fun stopOngoingCallDetection(promise: Promise) { + try { + stopOngoingCallDetectionInternal() + promise.resolve("Ongoing call detection stopped.") + } catch (e: Exception) { + promise.reject("NativePluginError", "Error in stopOngoingCallDetection: ${e.message}") + } + } + + private fun stopOngoingCallDetectionInternal() { + ongoingCallActivity?.get()?.let { Talsec.stopOngoingCallDetection(it) } + ongoingCallActivity = null + } + @ReactMethod fun storeExternalId( externalId: String, promise: Promise @@ -319,6 +469,9 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex private val mainHandler = Handler(Looper.getMainLooper()) internal var talsecStarted = false + + // Ongoing call detection is registered per activity + private var ongoingCallActivity: WeakReference? = null } internal class PluginListener(private val reactContext: ReactApplicationContext) : @@ -344,6 +497,13 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex } } + override fun ongoingCallDetected(state: CallState) { + val params = Arguments.createMap() + params.putInt(ThreatEvent.CHANNEL_KEY, ThreatEvent.OngoingCall.value) + params.putString(ThreatEvent.CALL_STATE_CHANNEL_KEY, state.name) + notifyEvent(ThreatEvent.CHANNEL_NAME, params) + } + override fun raspExecutionStateChanged(event: RaspExecutionStateEvent) { val params = Arguments.createMap() params.putInt(event.channelKey, event.value) diff --git a/android/src/main/java/com/freeraspreactnative/PluginThreatHandler.kt b/android/src/main/java/com/freeraspreactnative/PluginThreatHandler.kt index 3d7c29c..0b6c9d4 100644 --- a/android/src/main/java/com/freeraspreactnative/PluginThreatHandler.kt +++ b/android/src/main/java/com/freeraspreactnative/PluginThreatHandler.kt @@ -3,6 +3,7 @@ package com.freeraspreactnative import android.content.Context import app.talsec.rasp.security.api.SuspiciousAppInfo import app.talsec.rasp.security.api.ThreatListener +import app.talsec.rasp.security.api.ThreatListener.CallState import com.freeraspreactnative.dispatchers.ExecutionStateDispatcher import com.freeraspreactnative.dispatchers.ThreatDispatcher import com.freeraspreactnative.events.RaspExecutionStateEvent @@ -79,6 +80,10 @@ internal object PluginThreatHandler { override fun onBootloader() { ThreatDispatcher.dispatchThreat(ThreatEvent.Bootloader) } + + override fun onOngoingCall(state: CallState) { + ThreatDispatcher.dispatchOngoingCall(state) + } } private val deviceState = object : ThreatListener.DeviceState() { @@ -105,9 +110,19 @@ internal object PluginThreatHandler { } private val raspExecutionState = object : ThreatListener.RaspExecutionState() { - override fun onAllChecksFinished() { + override fun onCriticalChecksFinished() { + ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.CriticalChecksFinished) + } + + // Fast checks finish where the removed onAllChecksFinished() used to be triggered, so + // they keep driving the cross-platform allChecksFinished state. + override fun onFastChecksFinished() { ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.AllChecksFinished) } + + override fun onPostponedChecksFinished() { + ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.PostponedChecksFinished) + } } private val internalListener = ThreatListener(threatDetected, deviceState, raspExecutionState) diff --git a/android/src/main/java/com/freeraspreactnative/dispatchers/ThreatDispatcher.kt b/android/src/main/java/com/freeraspreactnative/dispatchers/ThreatDispatcher.kt index d5f8bea..5cb065b 100644 --- a/android/src/main/java/com/freeraspreactnative/dispatchers/ThreatDispatcher.kt +++ b/android/src/main/java/com/freeraspreactnative/dispatchers/ThreatDispatcher.kt @@ -1,6 +1,7 @@ package com.freeraspreactnative.dispatchers import app.talsec.rasp.security.api.SuspiciousAppInfo +import app.talsec.rasp.security.api.ThreatListener.CallState import com.freeraspreactnative.events.ThreatEvent import com.freeraspreactnative.interfaces.PluginThreatListener @@ -8,6 +9,7 @@ internal object ThreatDispatcher { private lateinit var listener: PluginThreatListener private val threatCache = mutableSetOf() private val malwareCache = mutableSetOf() + private val callStateCache = mutableListOf() private var isAppInForeground = false private var isListenerRegistered = false @@ -56,6 +58,17 @@ internal object ThreatDispatcher { } } + fun dispatchOngoingCall(state: CallState) { + if (isAppInForeground && isListenerRegistered) { + listener.ongoingCallDetected(state) + } else { + // Keep the order: a call that started and stopped in background is reported as both. + synchronized(callStateCache) { + callStateCache.add(state) + } + } + } + private fun flushCache() { val threats = synchronized(threatCache) { val snapshot = threatCache.toSet() @@ -72,6 +85,13 @@ internal object ThreatDispatcher { if (malware.isNotEmpty()) { listener.malwareDetected(malware) } + + val callStates = synchronized(callStateCache) { + val snapshot = callStateCache.toList() + callStateCache.clear() + snapshot + } + callStates.forEach { listener.ongoingCallDetected(it) } } } diff --git a/android/src/main/java/com/freeraspreactnative/events/RaspExecutionStateEvent.kt b/android/src/main/java/com/freeraspreactnative/events/RaspExecutionStateEvent.kt index eb9cfc8..50f11cd 100644 --- a/android/src/main/java/com/freeraspreactnative/events/RaspExecutionStateEvent.kt +++ b/android/src/main/java/com/freeraspreactnative/events/RaspExecutionStateEvent.kt @@ -7,12 +7,16 @@ internal sealed class RaspExecutionStateEvent(override val value: Int) : BaseRas override val channelKey: String get() = CHANNEL_KEY data object AllChecksFinished : RaspExecutionStateEvent(RandomGenerator.next()) + data object CriticalChecksFinished : RaspExecutionStateEvent(RandomGenerator.next()) + data object PostponedChecksFinished : RaspExecutionStateEvent(RandomGenerator.next()) companion object Companion { internal val CHANNEL_NAME = RandomGenerator.next().toString() internal val CHANNEL_KEY = RandomGenerator.next().toString() internal val ALL_EVENT_IDENTIFIERS = listOf( - AllChecksFinished + AllChecksFinished, + CriticalChecksFinished, + PostponedChecksFinished ).map { it.value } } } diff --git a/android/src/main/java/com/freeraspreactnative/events/ThreatEvent.kt b/android/src/main/java/com/freeraspreactnative/events/ThreatEvent.kt index 7e9fe5e..fe003c2 100644 --- a/android/src/main/java/com/freeraspreactnative/events/ThreatEvent.kt +++ b/android/src/main/java/com/freeraspreactnative/events/ThreatEvent.kt @@ -35,11 +35,13 @@ internal sealed class ThreatEvent(override val value: Int) : BaseRaspEvent { data object UnsecureWifi : ThreatEvent(RandomGenerator.next()) data object Automation : ThreatEvent(RandomGenerator.next()) data object Bootloader : ThreatEvent(RandomGenerator.next()) + data object OngoingCall : ThreatEvent(RandomGenerator.next()) companion object { internal val CHANNEL_NAME = RandomGenerator.next().toString() internal val CHANNEL_KEY = RandomGenerator.next().toString() internal val MALWARE_CHANNEL_KEY = RandomGenerator.next().toString() + internal val CALL_STATE_CHANNEL_KEY = RandomGenerator.next().toString() internal val ALL_EVENT_IDENTIFIERS = listOf( AppIntegrity, @@ -63,7 +65,8 @@ internal sealed class ThreatEvent(override val value: Int) : BaseRaspEvent { LocationSpoofing, UnsecureWifi, Automation, - Bootloader + Bootloader, + OngoingCall ).map { it.value } } } diff --git a/android/src/main/java/com/freeraspreactnative/interfaces/PluginThreatListener.kt b/android/src/main/java/com/freeraspreactnative/interfaces/PluginThreatListener.kt index cc949b3..843803f 100644 --- a/android/src/main/java/com/freeraspreactnative/interfaces/PluginThreatListener.kt +++ b/android/src/main/java/com/freeraspreactnative/interfaces/PluginThreatListener.kt @@ -1,9 +1,11 @@ package com.freeraspreactnative.interfaces import app.talsec.rasp.security.api.SuspiciousAppInfo +import app.talsec.rasp.security.api.ThreatListener.CallState import com.freeraspreactnative.events.ThreatEvent internal interface PluginThreatListener { fun threatDetected(threatEventType: ThreatEvent) fun malwareDetected(suspiciousApps: MutableList) + fun ongoingCallDetected(state: CallState) } diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 70fce86..f175588 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,10 @@ + + + + diff --git a/example/src/App.tsx b/example/src/App.tsx index 3fa5992..136b91f 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -3,7 +3,9 @@ import * as React from 'react'; import { Platform, PermissionsAndroid } from 'react-native'; import { addToWhitelist, + startOngoingCallDetection, useFreeRasp, + type CallState, type SuspiciousAppInfo, type TalsecConfig, } from 'freerasp-react-native'; @@ -22,10 +24,17 @@ const App = () => { const [allChecksStatus, setAllChecksStatus] = React.useState< 'in progress' | 'completed' >('in progress'); + const [postponedChecksStatus, setPostponedChecksStatus] = React.useState< + 'in progress' | 'completed' + >('in progress'); useEffect(() => { (async () => { - Platform.OS === 'android' && (await addItemsToMalwareWhitelist()); + if (Platform.OS === 'android') { + await addItemsToMalwareWhitelist(); + // Report cellular and VoIP calls through the ongoingCall action + await startOngoingCallDetection(); + } await requestLocationPermission(); })(); }, []); @@ -277,6 +286,17 @@ const App = () => { ) ); }, + // Android only + ongoingCall: (state: CallState) => { + console.info('Ongoing call:', state); + setAppChecks((currentState) => + currentState.map((threat) => + threat.name === 'Ongoing Call' + ? { ...threat, status: state === 'STOPPED' ? 'ok' : 'nok' } + : threat + ) + ); + }, }; const raspExecutionStateActions = { @@ -284,6 +304,10 @@ const App = () => { allChecksFinished: () => { setAllChecksStatus('completed'); }, + // Android only + postponedChecksFinished: () => { + setPostponedChecksStatus('completed'); + }, }; const requestLocationPermission = async () => { @@ -338,6 +362,7 @@ const App = () => { checks={appChecks} suspiciousApps={suspiciousApps} allChecksFinishedStatus={allChecksStatus} + postponedChecksFinishedStatus={postponedChecksStatus} /> ); }; diff --git a/example/src/DemoApp.tsx b/example/src/DemoApp.tsx index 1e72cb1..0091ea8 100644 --- a/example/src/DemoApp.tsx +++ b/example/src/DemoApp.tsx @@ -5,6 +5,7 @@ import CheckmarkCircle from '../assets/checkmark-circle-outline.png'; import CloseCircle from '../assets/close-circle-outline.png'; import TalsecLogo from '../assets/talsec-logo.png'; import { + Platform, ScrollView, Image, TouchableOpacity, @@ -19,6 +20,10 @@ import { MalwareModal } from './MalwareModal'; import { isScreenCaptureBlocked, blockScreenCapture, + isScreenControlBlocked, + blockScreenControl, + isOverlayWindowsBlocked, + blockOverlayWindows, type SuspiciousAppInfo, storeExternalId, removeExternalId, @@ -36,9 +41,19 @@ export const DemoApp: React.FC<{ }[]; suspiciousApps: SuspiciousAppInfo[]; allChecksFinishedStatus: 'in progress' | 'completed'; -}> = ({ checks, suspiciousApps, allChecksFinishedStatus }) => { + postponedChecksFinishedStatus: 'in progress' | 'completed'; +}> = ({ + checks, + suspiciousApps, + allChecksFinishedStatus, + postponedChecksFinishedStatus, +}) => { const [hasScreenCaptureBlocked, setHasScreenCaptureBlocked] = React.useState(false); + const [hasScreenControlBlocked, setHasScreenControlBlocked] = + React.useState(false); + const [hasOverlayWindowsBlocked, setHasOverlayWindowsBlocked] = + React.useState(false); const [externalIdValue, setExternalIdValue] = React.useState(''); const [modalVisible, setModalVisible] = React.useState(false); @@ -67,6 +82,75 @@ export const DemoApp: React.FC<{ } }; + const toggleScreenControlBlock = async () => { + try { + await blockScreenControl(!hasScreenControlBlocked); + setHasScreenControlBlocked(await isScreenControlBlocked()); + } catch (error: any) { + console.error('Screen control Error:', error); + } + }; + + const toggleOverlayWindowsBlock = async () => { + try { + const applied = await blockOverlayWindows(!hasOverlayWindowsBlocked); + console.info('Changing Overlay Windows Status applied:', applied); + setHasOverlayWindowsBlocked(await isOverlayWindowsBlocked()); + } catch (error: any) { + console.error('Overlay windows Error:', error); + } + }; + + const renderToggle = ( + isBlocked: boolean, + onPress: () => void, + blockLabel: string, + enableLabel: string + ) => ( + + + {isBlocked ? enableLabel : blockLabel} + + + ); + + const renderExecutionState = ( + label: string, + status: 'in progress' | 'completed' + ) => ( + + + + {label} + + {status === 'completed' ? ( + + ) : ( + + )} + + + ); + const handleModalSend = async () => { setModalVisible(false); try { @@ -174,45 +258,33 @@ export const DemoApp: React.FC<{ - RASP Execution State: - + {Platform.OS === 'android' && ( - - All Checks Finished - - {allChecksFinishedStatus === 'completed' ? ( - - ) : ( - + style={{ justifyContent: 'space-evenly', paddingTop: 10 }}> + {renderToggle( + hasScreenControlBlocked, + toggleScreenControlBlock, + 'Block Screen Control', + 'Enable Screen Control' + )} + {renderToggle( + hasOverlayWindowsBlocked, + toggleOverlayWindowsBlock, + 'Block Overlays', + 'Enable Overlays' )} - + )} + RASP Execution State: + {renderExecutionState( + 'All Checks Finished', + allChecksFinishedStatus + )} + {Platform.OS === 'android' && + renderExecutionState( + 'Postponed Checks Finished', + postponedChecksFinishedStatus + )} freeRASP checks: {checks.map((check: any, idx: number) => ( + typeof value === 'string' && callStates.includes(value); let isMappingPrepared = false; let isInitializing = false; @@ -29,7 +39,8 @@ export const setThreatListeners = async (config: ThreatEventActions) => { await removeThreatListener(); if (!threatChannel || !threatKey || !threatMalwareKey) { - [threatChannel, threatKey, threatMalwareKey] = await getThreatChannelData(); + [threatChannel, threatKey, threatMalwareKey, threatCallStateKey = null] = + await getThreatChannelData(); } if (!isMappingPrepared) { @@ -116,6 +127,16 @@ export const setThreatListeners = async (config: ThreatEventActions) => { case Threat.Bootloader.value: config.bootloader?.(); break; + case Threat.OngoingCall.value: + const callState = threatCallStateKey + ? event[threatCallStateKey] + : undefined; + if (!isCallState(callState)) { + onInvalidCallback(); + break; + } + config.ongoingCall?.(callState); + break; default: onInvalidCallback(); break; diff --git a/src/api/methods/native.ts b/src/api/methods/native.ts index cc3d089..7efa748 100644 --- a/src/api/methods/native.ts +++ b/src/api/methods/native.ts @@ -22,6 +22,74 @@ export const isScreenCaptureBlocked = (): Promise => { return FreeraspReactNative.isScreenCaptureBlocked(); }; +const androidOnly = (feature: string): Promise => + Promise.reject(`${feature} not available on iOS`); + +/** + * Android only. Prevents accessibility services from reading or controlling + * the app screen. + */ +export const blockScreenControl = (enable: boolean): Promise => { + if (Platform.OS === 'ios') { + return androidOnly('Screen control blocking'); + } + return FreeraspReactNative.blockScreenControl(enable); +}; + +/** + * Android only. Returns whether screen control is blocked. + */ +export const isScreenControlBlocked = (): Promise => { + if (Platform.OS === 'ios') { + return androidOnly('Screen control blocking'); + } + return FreeraspReactNative.isScreenControlBlocked(); +}; + +/** + * Android only. Hides overlay windows of other apps over the app screen. + * Requires Android 12+ and the `HIDE_OVERLAY_WINDOWS` permission. + * + * @returns whether the request was applied + */ +export const blockOverlayWindows = (enable: boolean): Promise => { + if (Platform.OS === 'ios') { + return androidOnly('Overlay windows blocking'); + } + return FreeraspReactNative.blockOverlayWindows(enable); +}; + +/** + * Android only. Returns whether overlay windows of other apps are hidden. + */ +export const isOverlayWindowsBlocked = (): Promise => { + if (Platform.OS === 'ios') { + return androidOnly('Overlay windows blocking'); + } + return FreeraspReactNative.isOverlayWindowsBlocked(); +}; + +/** + * Android only. Starts the ongoing call (cellular and VoIP) detection. + * Call states are reported through the `ongoingCall` threat action. + */ +export const startOngoingCallDetection = (): Promise => { + if (Platform.OS === 'ios') { + return androidOnly('Ongoing call detection'); + } + return FreeraspReactNative.startOngoingCallDetection(); +}; + +/** + * Android only. Stops the ongoing call detection. + */ +export const stopOngoingCallDetection = (): Promise => { + if (Platform.OS === 'ios') { + return androidOnly('Ongoing call detection'); + } + return FreeraspReactNative.stopOngoingCallDetection(); +}; + export const storeExternalId = (data: string): Promise => { return FreeraspReactNative.storeExternalId(data); }; diff --git a/src/channels/threat.ts b/src/channels/threat.ts index f63c8ba..7825c5c 100644 --- a/src/channels/threat.ts +++ b/src/channels/threat.ts @@ -1,3 +1,4 @@ +import { Platform } from 'react-native'; import { getThreatCount, itemsHaveType } from '../utils/utils'; import { FreeraspReactNative } from '../api/nativeModules'; import { Threat } from '../models/threat'; @@ -15,10 +16,12 @@ export const getThreatIdentifiers = async (): Promise => { }; export const getThreatChannelData = async (): Promise< - [string, string, string] + [string, string, string, string?] > => { + // Android also sends the key of the ongoing call state + const dataLength = Platform.OS === 'android' ? 4 : 3; const data = await FreeraspReactNative.getThreatChannelData(); - if (data.length !== 3 || !itemsHaveType(data, 'string')) { + if (data.length !== dataLength || !itemsHaveType(data, 'string')) { onInvalidCallback(); } return data; diff --git a/src/models/raspExecutionState.ts b/src/models/raspExecutionState.ts index a46c800..9fbfb45 100644 --- a/src/models/raspExecutionState.ts +++ b/src/models/raspExecutionState.ts @@ -1,13 +1,23 @@ +import { Platform } from 'react-native'; + export class RaspExecutionState { value: number; static AllChecksFinished = new RaspExecutionState(0); + static CriticalChecksFinished = new RaspExecutionState(0); + static PostponedChecksFinished = new RaspExecutionState(0); constructor(value: number) { this.value = value; } static getValues(): RaspExecutionState[] { - return [this.AllChecksFinished]; + return Platform.OS === 'android' + ? [ + this.AllChecksFinished, + this.CriticalChecksFinished, + this.PostponedChecksFinished, + ] + : [this.AllChecksFinished]; } } diff --git a/src/models/threat.ts b/src/models/threat.ts index 027033d..e7f6db0 100644 --- a/src/models/threat.ts +++ b/src/models/threat.ts @@ -26,6 +26,7 @@ export class Threat { static UnsecureWifi = new Threat(0); static Automation = new Threat(0); static Bootloader = new Threat(0); + static OngoingCall = new Threat(0); constructor(value: number) { this.value = value; @@ -56,6 +57,7 @@ export class Threat { this.UnsecureWifi, this.Automation, this.Bootloader, + this.OngoingCall, ] : [ this.AppIntegrity, diff --git a/src/types/types.ts b/src/types/types.ts index 93b1edf..5d4eda4 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -55,6 +55,15 @@ export type PackageInfo = { installerStore?: string; }; +/** + * State of an ongoing call reported by the ongoing call detection. + * + * - `STARTED` - a call started while the detection was running + * - `ONGOING` - a call was already in progress when the detection started + * - `STOPPED` - a previously reported call has ended + */ +export type CallState = 'STARTED' | 'ONGOING' | 'STOPPED'; + export type ThreatEventActions = { privilegedAccess?: () => any; debug?: () => any; @@ -79,10 +88,30 @@ export type ThreatEventActions = { unsecureWifi?: () => any; automation?: () => any; bootloader?: () => any; + /** + * Android only. Requires `startOngoingCallDetection()`. + */ + ongoingCall?: (state: CallState) => any; }; -export type NativeEvent = { [key: string]: number | string[] | undefined }; +export type NativeEvent = { + [key: string]: number | string | string[] | undefined; +}; export type RaspExecutionStateEventActions = { + /** + * All standard checks have been completed. On Android, triggered after the + * fast checks are completed, while the postponed checks still run. + */ allChecksFinished?: () => any; + /** + * Android only. The critical checks have been completed, before the + * non-critical checks. + */ + criticalChecksFinished?: () => any; + /** + * Android only. The postponed checks have been completed, which ends the + * initial evaluation. + */ + postponedChecksFinished?: () => any; }; From a85a7db239140937830157ec1f85c423e5bb6e85 Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Wed, 11 Nov 2026 09:38:05 +0100 Subject: [PATCH 2/9] docs: reword the Maven coordinates changelog entry Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b603f93..c917d68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `onFastChecksFinished()` is triggered after the fast standard checks are completed (where `onAllChecksFinished()` used to be triggered) - `onPostponedChecksFinished()` is triggered after the postponed/deep checks are completed - ❗️Breaking: Modified method `Talsec.isScreenCaptureBlocked(activity: Activity): Boolean` which now takes activity as parameter, and returns if the given activity view is protected against screen capture -- ❗️Breaking: Renamed Maven coordinates from `com.aheaditec.talsec.security:TalsecSecurity-{flavor}` +- ❗️Breaking: Deprecated the freeRASP Maven coordinates (`com.aheaditec.talsec.security:TalsecSecurity-{flavor}`) #### Added From b509745825a253fd45f35e111321f056162ee192 Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Wed, 11 Nov 2026 10:03:31 +0100 Subject: [PATCH 3/9] feat!: rename allChecksFinished to fastChecksFinished The execution state that fires when the fast (standard) checks complete is renamed on all platforms to match the Android SDK's onFastChecksFinished. On iOS it is driven by the SDK's onAllChecksFinished. This is a breaking change, so the package major version is bumped. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 7 +++++-- .../java/com/freeraspreactnative/PluginThreatHandler.kt | 4 +--- .../freeraspreactnative/events/RaspExecutionStateEvent.kt | 4 ++-- example/src/App.tsx | 4 ++-- example/src/DemoApp.tsx | 8 ++++---- ios/FreeraspReactNative.swift | 3 ++- ios/models/RaspExecutionStates.swift | 6 +++--- package.json | 2 +- src/api/listeners/raspExecutionState.ts | 4 ++-- src/models/raspExecutionState.ts | 6 +++--- src/types/types.ts | 7 ++++--- 11 files changed, 29 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c917d68..76a1785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,15 @@ 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). -## [5.4.0] - 2026-11-10 +## [6.0.0] - 2026-11-11 - Android SDK version: 20.0.1 - iOS SDK version: 8.0.1 +### Breaking + +- `allChecksFinished` RASP execution state action renamed to `fastChecksFinished`. On Android, it is triggered when the fast checks are completed, while the postponed checks may still run; on iOS, when all checks are completed + ### React Native #### Added @@ -21,7 +25,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Changed -- On Android, `allChecksFinished` is now triggered when the fast checks are completed, where it was triggered before; the postponed checks may still run afterwards - On Android, `isScreenCaptureBlocked()` reports the protection state of the current activity ### Android diff --git a/android/src/main/java/com/freeraspreactnative/PluginThreatHandler.kt b/android/src/main/java/com/freeraspreactnative/PluginThreatHandler.kt index 0b6c9d4..dd51070 100644 --- a/android/src/main/java/com/freeraspreactnative/PluginThreatHandler.kt +++ b/android/src/main/java/com/freeraspreactnative/PluginThreatHandler.kt @@ -114,10 +114,8 @@ internal object PluginThreatHandler { ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.CriticalChecksFinished) } - // Fast checks finish where the removed onAllChecksFinished() used to be triggered, so - // they keep driving the cross-platform allChecksFinished state. override fun onFastChecksFinished() { - ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.AllChecksFinished) + ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.FastChecksFinished) } override fun onPostponedChecksFinished() { diff --git a/android/src/main/java/com/freeraspreactnative/events/RaspExecutionStateEvent.kt b/android/src/main/java/com/freeraspreactnative/events/RaspExecutionStateEvent.kt index 50f11cd..6272383 100644 --- a/android/src/main/java/com/freeraspreactnative/events/RaspExecutionStateEvent.kt +++ b/android/src/main/java/com/freeraspreactnative/events/RaspExecutionStateEvent.kt @@ -6,7 +6,7 @@ internal sealed class RaspExecutionStateEvent(override val value: Int) : BaseRas override val channelName: String get() = CHANNEL_NAME override val channelKey: String get() = CHANNEL_KEY - data object AllChecksFinished : RaspExecutionStateEvent(RandomGenerator.next()) + data object FastChecksFinished : RaspExecutionStateEvent(RandomGenerator.next()) data object CriticalChecksFinished : RaspExecutionStateEvent(RandomGenerator.next()) data object PostponedChecksFinished : RaspExecutionStateEvent(RandomGenerator.next()) @@ -14,7 +14,7 @@ internal sealed class RaspExecutionStateEvent(override val value: Int) : BaseRas internal val CHANNEL_NAME = RandomGenerator.next().toString() internal val CHANNEL_KEY = RandomGenerator.next().toString() internal val ALL_EVENT_IDENTIFIERS = listOf( - AllChecksFinished, + FastChecksFinished, CriticalChecksFinished, PostponedChecksFinished ).map { it.value } diff --git a/example/src/App.tsx b/example/src/App.tsx index 136b91f..e588b11 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -301,7 +301,7 @@ const App = () => { const raspExecutionStateActions = { // Android & iOS - allChecksFinished: () => { + fastChecksFinished: () => { setAllChecksStatus('completed'); }, // Android only @@ -361,7 +361,7 @@ const App = () => { ); diff --git a/example/src/DemoApp.tsx b/example/src/DemoApp.tsx index 0091ea8..92645d0 100644 --- a/example/src/DemoApp.tsx +++ b/example/src/DemoApp.tsx @@ -40,12 +40,12 @@ export const DemoApp: React.FC<{ status: string; }[]; suspiciousApps: SuspiciousAppInfo[]; - allChecksFinishedStatus: 'in progress' | 'completed'; + fastChecksFinishedStatus: 'in progress' | 'completed'; postponedChecksFinishedStatus: 'in progress' | 'completed'; }> = ({ checks, suspiciousApps, - allChecksFinishedStatus, + fastChecksFinishedStatus, postponedChecksFinishedStatus, }) => { const [hasScreenCaptureBlocked, setHasScreenCaptureBlocked] = @@ -277,8 +277,8 @@ export const DemoApp: React.FC<{ )} RASP Execution State: {renderExecutionState( - 'All Checks Finished', - allChecksFinishedStatus + 'Fast Checks Finished', + fastChecksFinishedStatus )} {Platform.OS === 'android' && renderExecutionState( diff --git a/ios/FreeraspReactNative.swift b/ios/FreeraspReactNative.swift index 740ad50..2907137 100644 --- a/ios/FreeraspReactNative.swift +++ b/ios/FreeraspReactNative.swift @@ -172,8 +172,9 @@ extension SecurityThreatCenter: @retroactive SecurityThreatHandler, @retroactive ThreatDispatcher.shared.dispatch(threat: securityThreat) } + // The iOS SDK reports its standard checks as a single "all checks" event, which maps to fastChecksFinished. public func onAllChecksFinished() { - ExecutionStateDispatcher.shared.dispatch(event: RaspExecutionStates.allChecksFinished) + ExecutionStateDispatcher.shared.dispatch(event: RaspExecutionStates.fastChecksFinished) } } diff --git a/ios/models/RaspExecutionStates.swift b/ios/models/RaspExecutionStates.swift index 7ceda4e..1aea92d 100644 --- a/ios/models/RaspExecutionStates.swift +++ b/ios/models/RaspExecutionStates.swift @@ -1,14 +1,14 @@ enum RaspExecutionStates : String, Codable, CaseIterable, Equatable { - case allChecksFinished + case fastChecksFinished public static var allCases: [RaspExecutionStates] { - return [.allChecksFinished] + return [.fastChecksFinished] } var callbackIdentifier: Int { switch self { - case .allChecksFinished: + case .fastChecksFinished: return EventIdentifiers.raspExecutionStateIdentifierList[0] } } diff --git a/package.json b/package.json index 0da0d6e..6128c77 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "freerasp-react-native", - "version": "5.4.0", + "version": "6.0.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", diff --git a/src/api/listeners/raspExecutionState.ts b/src/api/listeners/raspExecutionState.ts index 11d059a..2e8bb8b 100644 --- a/src/api/listeners/raspExecutionState.ts +++ b/src/api/listeners/raspExecutionState.ts @@ -47,8 +47,8 @@ export const setRaspExecutionStateListener = async ( } switch (event[executionStateKey]) { - case RaspExecutionState.AllChecksFinished.value: - config.allChecksFinished?.(); + case RaspExecutionState.FastChecksFinished.value: + config.fastChecksFinished?.(); break; case RaspExecutionState.CriticalChecksFinished.value: config.criticalChecksFinished?.(); diff --git a/src/models/raspExecutionState.ts b/src/models/raspExecutionState.ts index 9fbfb45..3c60fd3 100644 --- a/src/models/raspExecutionState.ts +++ b/src/models/raspExecutionState.ts @@ -3,7 +3,7 @@ import { Platform } from 'react-native'; export class RaspExecutionState { value: number; - static AllChecksFinished = new RaspExecutionState(0); + static FastChecksFinished = new RaspExecutionState(0); static CriticalChecksFinished = new RaspExecutionState(0); static PostponedChecksFinished = new RaspExecutionState(0); @@ -14,10 +14,10 @@ export class RaspExecutionState { static getValues(): RaspExecutionState[] { return Platform.OS === 'android' ? [ - this.AllChecksFinished, + this.FastChecksFinished, this.CriticalChecksFinished, this.PostponedChecksFinished, ] - : [this.AllChecksFinished]; + : [this.FastChecksFinished]; } } diff --git a/src/types/types.ts b/src/types/types.ts index 5d4eda4..ddda6d8 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -100,10 +100,11 @@ export type NativeEvent = { export type RaspExecutionStateEventActions = { /** - * All standard checks have been completed. On Android, triggered after the - * fast checks are completed, while the postponed checks still run. + * The fast (standard) checks have been completed. On Android, the postponed + * checks may still run afterwards. On iOS, triggered when all checks have + * been completed. */ - allChecksFinished?: () => any; + fastChecksFinished?: () => any; /** * Android only. The critical checks have been completed, before the * non-critical checks. From d6f8fe2ad7748447976b2eb45cdb2552406b96d3 Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Wed, 11 Nov 2026 10:13:57 +0100 Subject: [PATCH 4/9] docs: drop Breaking markers from Android SDK changes absorbed by the plugin The plugin hides these native API changes from its users, so they move from the Android Breaking Changes section to Changed. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76a1785..f21c5d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,14 +29,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Android -#### Breaking Changes - -- ❗️Breaking: `ThreatListener.RaspExecutionState.onAllChecksFinished()` was removed and replaced by two callbacks: - - `onFastChecksFinished()` is triggered after the fast standard checks are completed (where `onAllChecksFinished()` used to be triggered) - - `onPostponedChecksFinished()` is triggered after the postponed/deep checks are completed -- ❗️Breaking: Modified method `Talsec.isScreenCaptureBlocked(activity: Activity): Boolean` which now takes activity as parameter, and returns if the given activity view is protected against screen capture -- ❗️Breaking: Deprecated the freeRASP Maven coordinates (`com.aheaditec.talsec.security:TalsecSecurity-{flavor}`) - #### Added - Added `isScreenControlBlocked(activity: Activity): Boolean` to check if the screen control protection is set for given activity @@ -59,6 +51,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Changed +- `ThreatListener.RaspExecutionState.onAllChecksFinished()` was removed and replaced by two callbacks: + - `onFastChecksFinished()` is triggered after the fast standard checks are completed (where `onAllChecksFinished()` used to be triggered) + - `onPostponedChecksFinished()` is triggered after the postponed/deep checks are completed +- Modified method `Talsec.isScreenCaptureBlocked(activity: Activity): Boolean` which now takes activity as parameter, and returns if the given activity view is protected against screen capture +- Deprecated the freeRASP Maven coordinates (`com.aheaditec.talsec.security:TalsecSecurity-{flavor}`) - Improved Bootloader detection - Extended emulator detection with additional device-property and ABI checks - Improved logging structure and data enrichment for better Portal insights/reports From 90e1b5d4f78b3cecc80f682b577fc1ef60048cee Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Wed, 11 Nov 2026 10:15:59 +0100 Subject: [PATCH 5/9] docs: drop Android changelog entries already listed in the plugin section Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f21c5d0..2ab9d2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,18 +29,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Android -#### Added - -- Added `isScreenControlBlocked(activity: Activity): Boolean` to check if the screen control protection is set for given activity -- Added `blockScreenControl(activity: Activity, enable: Boolean)` to prevent accessibility services from reading or controlling an activity's screen -- Added `blockOverlayWindows(activity: Activity, enable: Boolean): Boolean` to hide overlay windows of other apps over the given activity (Android 12+, requires `HIDE_OVERLAY_WINDOWS` permission) -- Added `isOverlayWindowsBlocked(activity: Activity): Boolean` to check if overlay windows are hidden for given activity -- Added a new `Ongoing Call` detection engine (VoIP, Cellular) - - started/paused per activity with `startOngoingCallDetection` / `stopOngoingCallDetection` - - new callback `onOngoingCall(CallState)` in `ThreatListener.ThreatDetected` - - new `CallState` enum with 3 call states (`STARTED`) -- Added callback `onCriticalChecksFinished()` in `ThreatListener.RaspExecutionState`, which is triggered after critical checks are completed - #### Fixed - Improved hook detection to prevent collisions with running VPN services and to better work with the device resources (threads/RAM) @@ -51,10 +39,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Changed -- `ThreatListener.RaspExecutionState.onAllChecksFinished()` was removed and replaced by two callbacks: - - `onFastChecksFinished()` is triggered after the fast standard checks are completed (where `onAllChecksFinished()` used to be triggered) - - `onPostponedChecksFinished()` is triggered after the postponed/deep checks are completed -- Modified method `Talsec.isScreenCaptureBlocked(activity: Activity): Boolean` which now takes activity as parameter, and returns if the given activity view is protected against screen capture - Deprecated the freeRASP Maven coordinates (`com.aheaditec.talsec.security:TalsecSecurity-{flavor}`) - Improved Bootloader detection - Extended emulator detection with additional device-property and ABI checks From 09cf0c75a1e8b2cc3037738e8980e8041b71c440 Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Wed, 11 Nov 2026 12:49:18 +0100 Subject: [PATCH 6/9] example: show the ongoing call state (Not detected, Started, Ongoing, Stopped) Co-Authored-By: Claude Opus 5.5 --- example/src/App.tsx | 9 ++++++++- example/src/DemoApp.tsx | 5 ++++- example/src/checks.ts | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index e588b11..9f7c23e 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -13,6 +13,9 @@ import { DemoApp } from './DemoApp'; import { commonChecks, iosChecks, androidChecks } from './checks'; import { useEffect } from 'react'; +const callStateLabel = (state: CallState) => + state.charAt(0) + state.slice(1).toLowerCase(); + const App = () => { const [appChecks, setAppChecks] = React.useState([ ...commonChecks, @@ -292,7 +295,11 @@ const App = () => { setAppChecks((currentState) => currentState.map((threat) => threat.name === 'Ongoing Call' - ? { ...threat, status: state === 'STOPPED' ? 'ok' : 'nok' } + ? { + ...threat, + status: state === 'STOPPED' ? 'ok' : 'nok', + detail: callStateLabel(state), + } : threat ) ); diff --git a/example/src/DemoApp.tsx b/example/src/DemoApp.tsx index 92645d0..599f8f2 100644 --- a/example/src/DemoApp.tsx +++ b/example/src/DemoApp.tsx @@ -38,6 +38,7 @@ export const DemoApp: React.FC<{ checks: { name: string; status: string; + detail?: string; }[]; suspiciousApps: SuspiciousAppInfo[]; fastChecksFinishedStatus: 'in progress' | 'completed'; @@ -303,7 +304,9 @@ export const DemoApp: React.FC<{ fontWeight: 'bold', alignSelf: 'center', }}> - {check.name} + {check.detail + ? `${check.name}: ${check.detail}` + : check.name} {check.name === 'Malware' && ( Date: Wed, 11 Nov 2026 12:51:51 +0100 Subject: [PATCH 7/9] fix(ios): send placeholder threat channel keys for Android-only payloads iOS now sends placeholder keys for malware and ongoing call, so the threat channel data has the same length on both platforms and the JS check no longer branches by platform. Co-Authored-By: Claude Opus 5.5 --- ios/FreeraspReactNative.swift | 3 ++- src/api/listeners/threat.ts | 15 +++++++++------ src/channels/threat.ts | 7 ++----- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/ios/FreeraspReactNative.swift b/ios/FreeraspReactNative.swift index 2907137..0667c28 100644 --- a/ios/FreeraspReactNative.swift +++ b/ios/FreeraspReactNative.swift @@ -107,7 +107,8 @@ class FreeraspReactNative: RCTEventEmitter { */ @objc(getThreatChannelData:withRejecter:) private func getThreatChannelData(resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) -> Void { - resolve([EventIdentifiers.threatChannelName, EventIdentifiers.threatChannelKey, "0"]) + // Malware and ongoing call are Android only, so iOS sends placeholders for their keys + resolve([EventIdentifiers.threatChannelName, EventIdentifiers.threatChannelKey, "0", "0"]) } /** diff --git a/src/api/listeners/threat.ts b/src/api/listeners/threat.ts index cd4e366..6e859c6 100644 --- a/src/api/listeners/threat.ts +++ b/src/api/listeners/threat.ts @@ -38,8 +38,13 @@ export const setThreatListeners = async (config: ThreatEventActions) => { await removeThreatListener(); - if (!threatChannel || !threatKey || !threatMalwareKey) { - [threatChannel, threatKey, threatMalwareKey, threatCallStateKey = null] = + if ( + !threatChannel || + !threatKey || + !threatMalwareKey || + !threatCallStateKey + ) { + [threatChannel, threatKey, threatMalwareKey, threatCallStateKey] = await getThreatChannelData(); } @@ -49,7 +54,7 @@ export const setThreatListeners = async (config: ThreatEventActions) => { } const listener = async (event: NativeEvent) => { - if (!threatKey || !threatMalwareKey) { + if (!threatKey || !threatMalwareKey || !threatCallStateKey) { onInvalidCallback(); return; } @@ -128,9 +133,7 @@ export const setThreatListeners = async (config: ThreatEventActions) => { config.bootloader?.(); break; case Threat.OngoingCall.value: - const callState = threatCallStateKey - ? event[threatCallStateKey] - : undefined; + const callState = event[threatCallStateKey]; if (!isCallState(callState)) { onInvalidCallback(); break; diff --git a/src/channels/threat.ts b/src/channels/threat.ts index 7825c5c..9942ac8 100644 --- a/src/channels/threat.ts +++ b/src/channels/threat.ts @@ -1,4 +1,3 @@ -import { Platform } from 'react-native'; import { getThreatCount, itemsHaveType } from '../utils/utils'; import { FreeraspReactNative } from '../api/nativeModules'; import { Threat } from '../models/threat'; @@ -16,12 +15,10 @@ export const getThreatIdentifiers = async (): Promise => { }; export const getThreatChannelData = async (): Promise< - [string, string, string, string?] + [string, string, string, string] > => { - // Android also sends the key of the ongoing call state - const dataLength = Platform.OS === 'android' ? 4 : 3; const data = await FreeraspReactNative.getThreatChannelData(); - if (data.length !== dataLength || !itemsHaveType(data, 'string')) { + if (data.length !== 4 || !itemsHaveType(data, 'string')) { onInvalidCallback(); } return data; From 8023a6552b76825c59a8c95d8e283369f6e07c90 Mon Sep 17 00:00:00 2001 From: Tomas Psota <72520867+tompsota@users.noreply.github.com> Date: Wed, 11 Nov 2026 18:30:42 +0100 Subject: [PATCH 8/9] refactor(android): move plugin to the app.talsec.rasp.reactnative namespace (#171) * refactor(android): move plugin to the app.talsec.rasp namespace Align the Android package of the plugin with the Android SDK's app.talsec.rasp, adding the platform as the last segment. Co-Authored-By: Claude Opus 5.5 * refactor(android): update package names to app.talsec.rasp Completes the move of the plugin sources: package declarations, imports, Gradle namespace and build configuration. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 1 + android/build.gradle | 4 +-- android/proguard-rules.pro | 6 ++-- android/src/main/AndroidManifest.xml | 2 +- .../reactnative}/FreeraspReactNativeModule.kt | 30 +++++++++---------- .../FreeraspReactNativePackage.kt | 2 +- .../rasp/reactnative}/PluginThreatHandler.kt | 10 +++---- .../rasp/reactnative}/ScreenProtector.kt | 2 +- .../dispatchers/ExecutionStateDispatcher.kt | 6 ++-- .../dispatchers/ThreatDispatcher.kt | 6 ++-- .../rasp/reactnative}/events/BaseRaspEvent.kt | 2 +- .../events/RaspExecutionStateEvent.kt | 4 +-- .../rasp/reactnative}/events/ThreatEvent.kt | 4 +-- .../exceptions/TalsecException.kt | 2 +- .../PluginExecutionStateListener.kt | 4 +-- .../interfaces/PluginThreatListener.kt | 4 +-- .../models/RNSuspiciousAppInfo.kt | 2 +- .../rasp/reactnative}/utils/Extensions.kt | 8 ++--- .../reactnative}/utils/RandomGenerator.kt | 2 +- .../talsec/rasp/reactnative}/utils/Utils.kt | 2 +- 20 files changed, 52 insertions(+), 51 deletions(-) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/FreeraspReactNativeModule.kt (94%) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/FreeraspReactNativePackage.kt (93%) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/PluginThreatHandler.kt (92%) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/ScreenProtector.kt (99%) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/dispatchers/ExecutionStateDispatcher.kt (85%) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/dispatchers/ThreatDispatcher.kt (93%) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/events/BaseRaspEvent.kt (71%) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/events/RaspExecutionStateEvent.kt (88%) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/events/ThreatEvent.kt (96%) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/exceptions/TalsecException.kt (70%) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/interfaces/PluginExecutionStateListener.kt (51%) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/interfaces/PluginThreatListener.kt (76%) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/models/RNSuspiciousAppInfo.kt (92%) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/utils/Extensions.kt (95%) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/utils/RandomGenerator.kt (92%) rename android/src/main/java/{com/freeraspreactnative => app/talsec/rasp/reactnative}/utils/Utils.kt (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ab9d2b..c729030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Changed +- Android package (namespace) of the plugin renamed from `com.freeraspreactnative` to `app.talsec.rasp.reactnative`, following the Android SDK's `app.talsec.rasp` (update the import if you register `FreeraspReactNativePackage` manually) - On Android, `isScreenCaptureBlocked()` reports the protection state of the current activity ### Android diff --git a/android/build.gradle b/android/build.gradle index bed0da1..47ac081 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -87,7 +87,7 @@ android { } if (project.android.hasProperty("namespace")) { - namespace("com.freeraspreactnative") + namespace("app.talsec.rasp.reactnative") } } @@ -119,6 +119,6 @@ if (isNewArchitectureEnabled()) { react { jsRootDir = file("../src/") libraryName = "FreeraspReactNative" - codegenJavaPackageName = "com.freeraspreactnative" + codegenJavaPackageName = "app.talsec.rasp.reactnative" } } diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro index 858660d..723be2a 100644 --- a/android/proguard-rules.pro +++ b/android/proguard-rules.pro @@ -1,12 +1,12 @@ -dontwarn java.lang.invoke.StringConcatFactory --keep class com.freeraspreactnative.FreeraspReactNativePackage { public *; } +-keep class app.talsec.rasp.reactnative.FreeraspReactNativePackage { public *; } -if @kotlinx.serialization.Serializable class ** -keep class <1> { *; } --keep class com.freeraspreactnative.models.RNSuspiciousAppInfo$Companion --keep class com.freeraspreactnative.models.RNPackageInfo$Companion +-keep class app.talsec.rasp.reactnative.models.RNSuspiciousAppInfo$Companion +-keep class app.talsec.rasp.reactnative.models.RNPackageInfo$Companion -repackageclasses 'ts' \ No newline at end of file diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 7db0be4..9e3ed50 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ + package="app.talsec.rasp.reactnative"> diff --git a/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt b/android/src/main/java/app/talsec/rasp/reactnative/FreeraspReactNativeModule.kt similarity index 94% rename from android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt rename to android/src/main/java/app/talsec/rasp/reactnative/FreeraspReactNativeModule.kt index a1163ff..2ff64f6 100644 --- a/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/FreeraspReactNativeModule.kt @@ -1,4 +1,4 @@ -package com.freeraspreactnative +package app.talsec.rasp.reactnative import android.annotation.SuppressLint import android.app.Activity @@ -23,20 +23,20 @@ import com.facebook.react.bridge.UiThreadUtil.runOnUiThread import com.facebook.react.bridge.WritableArray import com.facebook.react.bridge.WritableMap import com.facebook.react.modules.core.DeviceEventManagerModule -import com.freeraspreactnative.dispatchers.ExecutionStateDispatcher -import com.freeraspreactnative.dispatchers.ThreatDispatcher -import com.freeraspreactnative.events.RaspExecutionStateEvent -import com.freeraspreactnative.events.ThreatEvent -import com.freeraspreactnative.interfaces.PluginExecutionStateListener -import com.freeraspreactnative.interfaces.PluginThreatListener -import com.freeraspreactnative.utils.Utils -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 -import com.freeraspreactnative.utils.toSuspiciousAppDetectionConfig +import app.talsec.rasp.reactnative.dispatchers.ExecutionStateDispatcher +import app.talsec.rasp.reactnative.dispatchers.ThreatDispatcher +import app.talsec.rasp.reactnative.events.RaspExecutionStateEvent +import app.talsec.rasp.reactnative.events.ThreatEvent +import app.talsec.rasp.reactnative.interfaces.PluginExecutionStateListener +import app.talsec.rasp.reactnative.interfaces.PluginThreatListener +import app.talsec.rasp.reactnative.utils.Utils +import app.talsec.rasp.reactnative.utils.getArraySafe +import app.talsec.rasp.reactnative.utils.getBooleanSafe +import app.talsec.rasp.reactnative.utils.getMapThrowing +import app.talsec.rasp.reactnative.utils.getNestedArraySafe +import app.talsec.rasp.reactnative.utils.getStringThrowing +import app.talsec.rasp.reactnative.utils.toEncodedWritableArray +import app.talsec.rasp.reactnative.utils.toSuspiciousAppDetectionConfig import java.lang.ref.WeakReference class FreeraspReactNativeModule(private val reactContext: ReactApplicationContext) : diff --git a/android/src/main/java/com/freeraspreactnative/FreeraspReactNativePackage.kt b/android/src/main/java/app/talsec/rasp/reactnative/FreeraspReactNativePackage.kt similarity index 93% rename from android/src/main/java/com/freeraspreactnative/FreeraspReactNativePackage.kt rename to android/src/main/java/app/talsec/rasp/reactnative/FreeraspReactNativePackage.kt index 81b8f32..75b4ee5 100644 --- a/android/src/main/java/com/freeraspreactnative/FreeraspReactNativePackage.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/FreeraspReactNativePackage.kt @@ -1,4 +1,4 @@ -package com.freeraspreactnative +package app.talsec.rasp.reactnative import com.facebook.react.ReactPackage import com.facebook.react.bridge.NativeModule diff --git a/android/src/main/java/com/freeraspreactnative/PluginThreatHandler.kt b/android/src/main/java/app/talsec/rasp/reactnative/PluginThreatHandler.kt similarity index 92% rename from android/src/main/java/com/freeraspreactnative/PluginThreatHandler.kt rename to android/src/main/java/app/talsec/rasp/reactnative/PluginThreatHandler.kt index dd51070..cd96e29 100644 --- a/android/src/main/java/com/freeraspreactnative/PluginThreatHandler.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/PluginThreatHandler.kt @@ -1,13 +1,13 @@ -package com.freeraspreactnative +package app.talsec.rasp.reactnative import android.content.Context import app.talsec.rasp.security.api.SuspiciousAppInfo import app.talsec.rasp.security.api.ThreatListener import app.talsec.rasp.security.api.ThreatListener.CallState -import com.freeraspreactnative.dispatchers.ExecutionStateDispatcher -import com.freeraspreactnative.dispatchers.ThreatDispatcher -import com.freeraspreactnative.events.RaspExecutionStateEvent -import com.freeraspreactnative.events.ThreatEvent +import app.talsec.rasp.reactnative.dispatchers.ExecutionStateDispatcher +import app.talsec.rasp.reactnative.dispatchers.ThreatDispatcher +import app.talsec.rasp.reactnative.events.RaspExecutionStateEvent +import app.talsec.rasp.reactnative.events.ThreatEvent internal object PluginThreatHandler { diff --git a/android/src/main/java/com/freeraspreactnative/ScreenProtector.kt b/android/src/main/java/app/talsec/rasp/reactnative/ScreenProtector.kt similarity index 99% rename from android/src/main/java/com/freeraspreactnative/ScreenProtector.kt rename to android/src/main/java/app/talsec/rasp/reactnative/ScreenProtector.kt index 489660c..cbe6387 100644 --- a/android/src/main/java/com/freeraspreactnative/ScreenProtector.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/ScreenProtector.kt @@ -1,4 +1,4 @@ -package com.freeraspreactnative +package app.talsec.rasp.reactnative import android.annotation.SuppressLint import android.annotation.TargetApi diff --git a/android/src/main/java/com/freeraspreactnative/dispatchers/ExecutionStateDispatcher.kt b/android/src/main/java/app/talsec/rasp/reactnative/dispatchers/ExecutionStateDispatcher.kt similarity index 85% rename from android/src/main/java/com/freeraspreactnative/dispatchers/ExecutionStateDispatcher.kt rename to android/src/main/java/app/talsec/rasp/reactnative/dispatchers/ExecutionStateDispatcher.kt index 1c4d8c7..14bab19 100644 --- a/android/src/main/java/com/freeraspreactnative/dispatchers/ExecutionStateDispatcher.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/dispatchers/ExecutionStateDispatcher.kt @@ -1,7 +1,7 @@ -package com.freeraspreactnative.dispatchers +package app.talsec.rasp.reactnative.dispatchers -import com.freeraspreactnative.events.RaspExecutionStateEvent -import com.freeraspreactnative.interfaces.PluginExecutionStateListener +import app.talsec.rasp.reactnative.events.RaspExecutionStateEvent +import app.talsec.rasp.reactnative.interfaces.PluginExecutionStateListener internal object ExecutionStateDispatcher { private lateinit var listener: PluginExecutionStateListener diff --git a/android/src/main/java/com/freeraspreactnative/dispatchers/ThreatDispatcher.kt b/android/src/main/java/app/talsec/rasp/reactnative/dispatchers/ThreatDispatcher.kt similarity index 93% rename from android/src/main/java/com/freeraspreactnative/dispatchers/ThreatDispatcher.kt rename to android/src/main/java/app/talsec/rasp/reactnative/dispatchers/ThreatDispatcher.kt index 5cb065b..8c0c6b7 100644 --- a/android/src/main/java/com/freeraspreactnative/dispatchers/ThreatDispatcher.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/dispatchers/ThreatDispatcher.kt @@ -1,9 +1,9 @@ -package com.freeraspreactnative.dispatchers +package app.talsec.rasp.reactnative.dispatchers import app.talsec.rasp.security.api.SuspiciousAppInfo import app.talsec.rasp.security.api.ThreatListener.CallState -import com.freeraspreactnative.events.ThreatEvent -import com.freeraspreactnative.interfaces.PluginThreatListener +import app.talsec.rasp.reactnative.events.ThreatEvent +import app.talsec.rasp.reactnative.interfaces.PluginThreatListener internal object ThreatDispatcher { private lateinit var listener: PluginThreatListener diff --git a/android/src/main/java/com/freeraspreactnative/events/BaseRaspEvent.kt b/android/src/main/java/app/talsec/rasp/reactnative/events/BaseRaspEvent.kt similarity index 71% rename from android/src/main/java/com/freeraspreactnative/events/BaseRaspEvent.kt rename to android/src/main/java/app/talsec/rasp/reactnative/events/BaseRaspEvent.kt index dc42359..d4a1db4 100644 --- a/android/src/main/java/com/freeraspreactnative/events/BaseRaspEvent.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/events/BaseRaspEvent.kt @@ -1,4 +1,4 @@ -package com.freeraspreactnative.events +package app.talsec.rasp.reactnative.events internal interface BaseRaspEvent { val value: Int diff --git a/android/src/main/java/com/freeraspreactnative/events/RaspExecutionStateEvent.kt b/android/src/main/java/app/talsec/rasp/reactnative/events/RaspExecutionStateEvent.kt similarity index 88% rename from android/src/main/java/com/freeraspreactnative/events/RaspExecutionStateEvent.kt rename to android/src/main/java/app/talsec/rasp/reactnative/events/RaspExecutionStateEvent.kt index 6272383..5a9dadf 100644 --- a/android/src/main/java/com/freeraspreactnative/events/RaspExecutionStateEvent.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/events/RaspExecutionStateEvent.kt @@ -1,6 +1,6 @@ -package com.freeraspreactnative.events +package app.talsec.rasp.reactnative.events -import com.freeraspreactnative.utils.RandomGenerator +import app.talsec.rasp.reactnative.utils.RandomGenerator internal sealed class RaspExecutionStateEvent(override val value: Int) : BaseRaspEvent { override val channelName: String get() = CHANNEL_NAME diff --git a/android/src/main/java/com/freeraspreactnative/events/ThreatEvent.kt b/android/src/main/java/app/talsec/rasp/reactnative/events/ThreatEvent.kt similarity index 96% rename from android/src/main/java/com/freeraspreactnative/events/ThreatEvent.kt rename to android/src/main/java/app/talsec/rasp/reactnative/events/ThreatEvent.kt index fe003c2..e43725a 100644 --- a/android/src/main/java/com/freeraspreactnative/events/ThreatEvent.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/events/ThreatEvent.kt @@ -1,6 +1,6 @@ -package com.freeraspreactnative.events +package app.talsec.rasp.reactnative.events -import com.freeraspreactnative.utils.RandomGenerator +import app.talsec.rasp.reactnative.utils.RandomGenerator /** * Sealed class to represent the error codes. diff --git a/android/src/main/java/com/freeraspreactnative/exceptions/TalsecException.kt b/android/src/main/java/app/talsec/rasp/reactnative/exceptions/TalsecException.kt similarity index 70% rename from android/src/main/java/com/freeraspreactnative/exceptions/TalsecException.kt rename to android/src/main/java/app/talsec/rasp/reactnative/exceptions/TalsecException.kt index 69c76cd..ce38cc6 100644 --- a/android/src/main/java/com/freeraspreactnative/exceptions/TalsecException.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/exceptions/TalsecException.kt @@ -1,3 +1,3 @@ -package com.freeraspreactnative.exceptions +package app.talsec.rasp.reactnative.exceptions class TalsecException(message: String, val code: String? = null, val ex: Exception? = null) : Exception(message) diff --git a/android/src/main/java/com/freeraspreactnative/interfaces/PluginExecutionStateListener.kt b/android/src/main/java/app/talsec/rasp/reactnative/interfaces/PluginExecutionStateListener.kt similarity index 51% rename from android/src/main/java/com/freeraspreactnative/interfaces/PluginExecutionStateListener.kt rename to android/src/main/java/app/talsec/rasp/reactnative/interfaces/PluginExecutionStateListener.kt index 7f10f31..d7f6efb 100644 --- a/android/src/main/java/com/freeraspreactnative/interfaces/PluginExecutionStateListener.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/interfaces/PluginExecutionStateListener.kt @@ -1,6 +1,6 @@ -package com.freeraspreactnative.interfaces +package app.talsec.rasp.reactnative.interfaces -import com.freeraspreactnative.events.RaspExecutionStateEvent +import app.talsec.rasp.reactnative.events.RaspExecutionStateEvent internal interface PluginExecutionStateListener { fun raspExecutionStateChanged(event: RaspExecutionStateEvent) diff --git a/android/src/main/java/com/freeraspreactnative/interfaces/PluginThreatListener.kt b/android/src/main/java/app/talsec/rasp/reactnative/interfaces/PluginThreatListener.kt similarity index 76% rename from android/src/main/java/com/freeraspreactnative/interfaces/PluginThreatListener.kt rename to android/src/main/java/app/talsec/rasp/reactnative/interfaces/PluginThreatListener.kt index 843803f..4d51bfa 100644 --- a/android/src/main/java/com/freeraspreactnative/interfaces/PluginThreatListener.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/interfaces/PluginThreatListener.kt @@ -1,8 +1,8 @@ -package com.freeraspreactnative.interfaces +package app.talsec.rasp.reactnative.interfaces import app.talsec.rasp.security.api.SuspiciousAppInfo import app.talsec.rasp.security.api.ThreatListener.CallState -import com.freeraspreactnative.events.ThreatEvent +import app.talsec.rasp.reactnative.events.ThreatEvent internal interface PluginThreatListener { fun threatDetected(threatEventType: ThreatEvent) diff --git a/android/src/main/java/com/freeraspreactnative/models/RNSuspiciousAppInfo.kt b/android/src/main/java/app/talsec/rasp/reactnative/models/RNSuspiciousAppInfo.kt similarity index 92% rename from android/src/main/java/com/freeraspreactnative/models/RNSuspiciousAppInfo.kt rename to android/src/main/java/app/talsec/rasp/reactnative/models/RNSuspiciousAppInfo.kt index 4c6b0e9..20cddc9 100644 --- a/android/src/main/java/com/freeraspreactnative/models/RNSuspiciousAppInfo.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/models/RNSuspiciousAppInfo.kt @@ -1,4 +1,4 @@ -package com.freeraspreactnative.models +package app.talsec.rasp.reactnative.models import kotlinx.serialization.Serializable diff --git a/android/src/main/java/com/freeraspreactnative/utils/Extensions.kt b/android/src/main/java/app/talsec/rasp/reactnative/utils/Extensions.kt similarity index 95% rename from android/src/main/java/com/freeraspreactnative/utils/Extensions.kt rename to android/src/main/java/app/talsec/rasp/reactnative/utils/Extensions.kt index 2da0016..04cf57b 100644 --- a/android/src/main/java/com/freeraspreactnative/utils/Extensions.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/utils/Extensions.kt @@ -1,4 +1,4 @@ -package com.freeraspreactnative.utils +package app.talsec.rasp.reactnative.utils import android.content.pm.PackageInfo import android.util.Base64 @@ -13,9 +13,9 @@ 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 app.talsec.rasp.reactnative.exceptions.TalsecException +import app.talsec.rasp.reactnative.models.RNPackageInfo +import app.talsec.rasp.reactnative.models.RNSuspiciousAppInfo import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.json.JSONException diff --git a/android/src/main/java/com/freeraspreactnative/utils/RandomGenerator.kt b/android/src/main/java/app/talsec/rasp/reactnative/utils/RandomGenerator.kt similarity index 92% rename from android/src/main/java/com/freeraspreactnative/utils/RandomGenerator.kt rename to android/src/main/java/app/talsec/rasp/reactnative/utils/RandomGenerator.kt index 6860ca9..e1713c5 100644 --- a/android/src/main/java/com/freeraspreactnative/utils/RandomGenerator.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/utils/RandomGenerator.kt @@ -1,4 +1,4 @@ -package com.freeraspreactnative.utils +package app.talsec.rasp.reactnative.utils import java.security.SecureRandom diff --git a/android/src/main/java/com/freeraspreactnative/utils/Utils.kt b/android/src/main/java/app/talsec/rasp/reactnative/utils/Utils.kt similarity index 98% rename from android/src/main/java/com/freeraspreactnative/utils/Utils.kt rename to android/src/main/java/app/talsec/rasp/reactnative/utils/Utils.kt index 6d0d954..d875bbc 100644 --- a/android/src/main/java/com/freeraspreactnative/utils/Utils.kt +++ b/android/src/main/java/app/talsec/rasp/reactnative/utils/Utils.kt @@ -1,4 +1,4 @@ -package com.freeraspreactnative.utils +package app.talsec.rasp.reactnative.utils import android.content.pm.ApplicationInfo import android.graphics.Bitmap From fa3757c6b8a419244eba8a9520c09d012719dbce Mon Sep 17 00:00:00 2001 From: Tomas Psota <72520867+tompsota@users.noreply.github.com> Date: Thu, 12 Nov 2026 10:26:42 +0100 Subject: [PATCH 9/9] feat!: unify the public API with the other freeRASP plugins (#172) * feat!: unify the public API with the other freeRASP plugins - TalsecIosConfig.appBundleId (string) -> appBundleIds (string[]), as in the iOS SDK - methods that resolved with status strings or constants resolve to void - all methods reject with TalsecError carrying a stable code, including Android-only methods called on iOS - onInvalidCallback and removeListenerForEvent are no longer exported; abortApp is the only exported internal helper Co-Authored-By: Claude Opus 5.5 * feat: return start status and errors from useFreeRasp useFreeRasp returns { started, error } instead of only logging failures, so apps can react when the listeners can't be registered or freeRASP fails to start. Co-Authored-By: Claude Opus 5.5 * refactor(types): type threat and execution state actions as returning void Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 6 ++ example/src/App.tsx | 22 +++-- example/src/DemoApp.tsx | 3 +- example/src/MalwareItem.tsx | 6 +- ios/FreeraspReactNative.swift | 6 +- src/api/listeners/raspExecutionState.ts | 2 +- src/api/listeners/threat.ts | 2 +- src/api/methods/internal.ts | 13 +++ src/api/methods/native.ts | 108 ++++++++++++++++-------- src/api/methods/reactNative.ts | 62 ++++++++++---- src/channels/raspExecutionState.ts | 2 +- src/channels/threat.ts | 2 +- src/index.tsx | 2 + src/types/errors.ts | 57 +++++++++++++ src/types/types.ts | 68 +++++++++------ 15 files changed, 262 insertions(+), 99 deletions(-) create mode 100644 src/api/methods/internal.ts create mode 100644 src/types/errors.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c729030..e36f2ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,11 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Breaking - `allChecksFinished` RASP execution state action renamed to `fastChecksFinished`. On Android, it is triggered when the fast checks are completed, while the postponed checks may still run; on iOS, when all checks are completed +- `TalsecIosConfig.appBundleId` (string) replaced by `appBundleIds` (string[]), matching the iOS SDK +- Methods that resolved with a status string or a constant now resolve to `void`: `talsecStart`, `addToWhitelist`, `blockScreenCapture`, `blockScreenControl`, `startOngoingCallDetection`, `stopOngoingCallDetection`, `storeExternalId`, `removeExternalId` +- All methods now reject with `TalsecError` (`code`: `talsec-failure`, `unsupported-platform`, `configuration-exception`, `external-id-failure`, `malware-failure`) instead of native errors or plain strings +- `onInvalidCallback` and `removeListenerForEvent` are no longer exported; use `abortApp` to terminate the app ### React Native #### Added +- `useFreeRasp` returns `{ started, error }`; `error` is the `TalsecError` that prevented the listeners from being registered or freeRASP from starting, which was previously only logged to the console - `ongoingCall` threat action with the new `CallState` type (`'STARTED' | 'ONGOING' | 'STOPPED'`), driven by `startOngoingCallDetection()` and `stopOngoingCallDetection()` (Android only) - `blockScreenControl()` and `isScreenControlBlocked()` to prevent accessibility services from reading or controlling the app screen (Android only) - `blockOverlayWindows()` and `isOverlayWindowsBlocked()` to hide overlay windows of other apps (Android 12+ only, requires the `HIDE_OVERLAY_WINDOWS` permission) @@ -27,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Android package (namespace) of the plugin renamed from `com.freeraspreactnative` to `app.talsec.rasp.reactnative`, following the Android SDK's `app.talsec.rasp` (update the import if you register `FreeraspReactNativePackage` manually) - On Android, `isScreenCaptureBlocked()` reports the protection state of the current activity +- Threat and RASP execution state actions are typed as returning `void` instead of `any` ### Android diff --git a/example/src/App.tsx b/example/src/App.tsx index 9f7c23e..78661a5 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -67,7 +67,7 @@ const App = () => { }, }, iosConfig: { - appBundleId: 'org.reactjs.native.example.FreeraspReactNativeExample', + appBundleIds: ['org.reactjs.native.example.FreeraspReactNativeExample'], appTeamId: 'your_team_ID', }, watcherMail: 'your_email_address@example.com', @@ -351,10 +351,8 @@ const App = () => { await Promise.all( appsToWhitelist.map(async (app) => { try { - const whitelistResponse = await addToWhitelist(app); - console.info( - `${app} stored to Malware Whitelist: ${whitelistResponse}` - ); + await addToWhitelist(app); + console.info(`${app} stored to Malware Whitelist`); } catch (error) { console.info('Malware whitelist failed: ', error); } @@ -362,7 +360,19 @@ const App = () => { ); }; - useFreeRasp(config, actions, raspExecutionStateActions); + const { error: raspError } = useFreeRasp( + config, + actions, + raspExecutionStateActions + ); + + useEffect(() => { + if (raspError) { + console.error( + `freeRASP failed to start: ${raspError.code}: ${raspError.message}` + ); + } + }, [raspError]); return ( { try { - const response = await blockScreenCapture(!hasScreenCaptureBlocked); - console.info('Changing Screen Capture Status:', response); + await blockScreenCapture(!hasScreenCaptureBlocked); await updateScreenCaptureStatus(); } catch (error: any) { console.error('Screen capture Error:', error.message); diff --git a/example/src/MalwareItem.tsx b/example/src/MalwareItem.tsx index ce7c311..20d5190 100644 --- a/example/src/MalwareItem.tsx +++ b/example/src/MalwareItem.tsx @@ -28,10 +28,8 @@ export const MalwareItem: React.FC<{ app: SuspiciousAppInfo }> = ({ app }) => { const whitelistApp = async (packageName: string) => { try { - const whitelistResponse = await addToWhitelist(packageName); - console.info( - `Malware Whitelist response for ${app}: ${whitelistResponse}` - ); + await addToWhitelist(packageName); + console.info(`${app.packageInfo.packageName} added to Malware Whitelist`); alert('Restart app for whitelist to take effect'); } catch (error: any) { console.info('Error while adding app to malware whitelist: ', error); diff --git a/ios/FreeraspReactNative.swift b/ios/FreeraspReactNative.swift index 0667c28..e625ddf 100644 --- a/ios/FreeraspReactNative.swift +++ b/ios/FreeraspReactNative.swift @@ -51,8 +51,8 @@ class FreeraspReactNative: RCTEventEmitter { guard let iosConfig = talsecConfig["iosConfig"] as? NSDictionary else { throw NSError(domain: "Missing iosConfig parameter in Talsec Native Plugin", code: 1) } - guard let appBundleIds = iosConfig["appBundleId"] as? String else { - throw NSError(domain: "Missing appBundleId parameter in Talsec Native Plugin", code: 2) + guard let appBundleIds = iosConfig["appBundleIds"] as? [String] else { + throw NSError(domain: "Missing appBundleIds parameter in Talsec Native Plugin", code: 2) } guard let appTeamId = iosConfig["appTeamId"] as? String else { throw NSError(domain: "Missing appTeamId parameter in Talsec Native Plugin", code: 3) @@ -62,7 +62,7 @@ class FreeraspReactNative: RCTEventEmitter { } let isProd = talsecConfig["isProd"] as? Bool ?? true - let config = TalsecConfig(appBundleIds: [appBundleIds], appTeamId: appTeamId, watcherMailAddress: watcherMailAddress, isProd: isProd) + let config = TalsecConfig(appBundleIds: appBundleIds, appTeamId: appTeamId, watcherMailAddress: watcherMailAddress, isProd: isProd) Talsec.start(config: config) } diff --git a/src/api/listeners/raspExecutionState.ts b/src/api/listeners/raspExecutionState.ts index 2e8bb8b..9552852 100644 --- a/src/api/listeners/raspExecutionState.ts +++ b/src/api/listeners/raspExecutionState.ts @@ -1,6 +1,6 @@ import { NativeEventEmitter, type EmitterSubscription } from 'react-native'; import { FreeraspReactNative } from '../nativeModules'; -import { onInvalidCallback, removeListenerForEvent } from '../methods/native'; +import { onInvalidCallback, removeListenerForEvent } from '../methods/internal'; import type { NativeEvent, RaspExecutionStateEventActions, diff --git a/src/api/listeners/threat.ts b/src/api/listeners/threat.ts index 6e859c6..99b1e6b 100644 --- a/src/api/listeners/threat.ts +++ b/src/api/listeners/threat.ts @@ -1,6 +1,6 @@ import { NativeEventEmitter, type EmitterSubscription } from 'react-native'; import { FreeraspReactNative } from '../nativeModules'; -import { onInvalidCallback, removeListenerForEvent } from '../methods/native'; +import { onInvalidCallback, removeListenerForEvent } from '../methods/internal'; import { Threat } from '../../models/threat'; import type { CallState, diff --git a/src/api/methods/internal.ts b/src/api/methods/internal.ts new file mode 100644 index 0000000..7b4fb32 --- /dev/null +++ b/src/api/methods/internal.ts @@ -0,0 +1,13 @@ +import { FreeraspReactNative } from '../nativeModules'; + +/** + * Terminates the app. Called when the native side sends data that freeRASP + * never sends, which indicates tampering. Exported publicly as `abortApp`. + */ +export const onInvalidCallback = (): void => { + FreeraspReactNative.onInvalidCallback(); +}; + +export const removeListenerForEvent = (channel: string): Promise => { + return FreeraspReactNative.removeListenerForEvent(channel); +}; diff --git a/src/api/methods/native.ts b/src/api/methods/native.ts index 7efa748..ef77da3 100644 --- a/src/api/methods/native.ts +++ b/src/api/methods/native.ts @@ -1,39 +1,60 @@ import { Platform } from 'react-native'; import { FreeraspReactNative } from '../nativeModules'; import type { TalsecConfig } from '../../types/types'; +import { androidOnly, wrapNative } from '../../types/errors'; import { withDefaults } from '../../utils/config'; -export const talsecStart = async (options: TalsecConfig): Promise => { - return FreeraspReactNative.talsecStart(withDefaults(options)); +/** + * Starts freeRASP. Prefer the `useFreeRasp` hook, which also registers the + * listeners. + */ +export const talsecStart = async (options: TalsecConfig): Promise => { + await wrapNative('talsec-failure', () => + FreeraspReactNative.talsecStart(withDefaults(options)) + ); }; -export const addToWhitelist = async (packageName: string): Promise => { +/** + * Android only. Adds an app to the malware whitelist so it is not reported. + */ +export const addToWhitelist = async (packageName: string): Promise => { if (Platform.OS === 'ios') { - return Promise.reject('Malware detection not available on iOS'); + return androidOnly('Malware detection'); } - return FreeraspReactNative.addToWhitelist(packageName); + await wrapNative('malware-failure', () => + FreeraspReactNative.addToWhitelist(packageName) + ); }; -export const blockScreenCapture = (enable: boolean): Promise => { - return FreeraspReactNative.blockScreenCapture(enable); +/** + * Blocks or unblocks screenshots and screen recording of the app screen. + */ +export const blockScreenCapture = async (enable: boolean): Promise => { + await wrapNative('talsec-failure', () => + FreeraspReactNative.blockScreenCapture(enable) + ); }; +/** + * Returns whether screen capture is blocked for the current screen. + */ export const isScreenCaptureBlocked = (): Promise => { - return FreeraspReactNative.isScreenCaptureBlocked(); + return wrapNative('talsec-failure', () => + FreeraspReactNative.isScreenCaptureBlocked() + ); }; -const androidOnly = (feature: string): Promise => - Promise.reject(`${feature} not available on iOS`); - /** * Android only. Prevents accessibility services from reading or controlling * the app screen. */ -export const blockScreenControl = (enable: boolean): Promise => { +export const blockScreenControl = async (enable: boolean): Promise => { if (Platform.OS === 'ios') { return androidOnly('Screen control blocking'); } - return FreeraspReactNative.blockScreenControl(enable); + await wrapNative('talsec-failure', () => + FreeraspReactNative.blockScreenControl(enable) + ); }; /** @@ -43,7 +64,9 @@ export const isScreenControlBlocked = (): Promise => { if (Platform.OS === 'ios') { return androidOnly('Screen control blocking'); } - return FreeraspReactNative.isScreenControlBlocked(); + return wrapNative('talsec-failure', () => + FreeraspReactNative.isScreenControlBlocked() + ); }; /** @@ -56,7 +79,9 @@ export const blockOverlayWindows = (enable: boolean): Promise => { if (Platform.OS === 'ios') { return androidOnly('Overlay windows blocking'); } - return FreeraspReactNative.blockOverlayWindows(enable); + return wrapNative('talsec-failure', () => + FreeraspReactNative.blockOverlayWindows(enable) + ); }; /** @@ -66,51 +91,62 @@ export const isOverlayWindowsBlocked = (): Promise => { if (Platform.OS === 'ios') { return androidOnly('Overlay windows blocking'); } - return FreeraspReactNative.isOverlayWindowsBlocked(); + return wrapNative('talsec-failure', () => + FreeraspReactNative.isOverlayWindowsBlocked() + ); }; /** * Android only. Starts the ongoing call (cellular and VoIP) detection. * Call states are reported through the `ongoingCall` threat action. */ -export const startOngoingCallDetection = (): Promise => { +export const startOngoingCallDetection = async (): Promise => { if (Platform.OS === 'ios') { return androidOnly('Ongoing call detection'); } - return FreeraspReactNative.startOngoingCallDetection(); + await wrapNative('talsec-failure', () => + FreeraspReactNative.startOngoingCallDetection() + ); }; /** * Android only. Stops the ongoing call detection. */ -export const stopOngoingCallDetection = (): Promise => { +export const stopOngoingCallDetection = async (): Promise => { if (Platform.OS === 'ios') { return androidOnly('Ongoing call detection'); } - return FreeraspReactNative.stopOngoingCallDetection(); + await wrapNative('talsec-failure', () => + FreeraspReactNative.stopOngoingCallDetection() + ); }; -export const storeExternalId = (data: string): Promise => { - return FreeraspReactNative.storeExternalId(data); +/** + * Stores an external ID sent with freeRASP logs. + */ +export const storeExternalId = async (data: string): Promise => { + await wrapNative('external-id-failure', () => + FreeraspReactNative.storeExternalId(data) + ); }; -export const removeExternalId = (): Promise => { - return FreeraspReactNative.removeExternalId(); +/** + * Removes the stored external ID. + */ +export const removeExternalId = async (): Promise => { + await wrapNative('external-id-failure', () => + FreeraspReactNative.removeExternalId() + ); }; +/** + * Android only. Returns the icon of the given app as a base64-encoded PNG. + */ export const getAppIcon = (packageName: string): Promise => { if (Platform.OS === 'ios') { - return Promise.reject( - 'App icon retrieval for Malware detection not available on iOS' - ); + return androidOnly('App icon retrieval for malware detection'); } - return FreeraspReactNative.getAppIcon(packageName); -}; - -export const onInvalidCallback = (): void => { - FreeraspReactNative.onInvalidCallback(); -}; - -export const removeListenerForEvent = (channel: string): Promise => { - return FreeraspReactNative.removeListenerForEvent(channel); + return wrapNative('malware-failure', () => + FreeraspReactNative.getAppIcon(packageName) + ); }; diff --git a/src/api/methods/reactNative.ts b/src/api/methods/reactNative.ts index 4379446..860feb4 100644 --- a/src/api/methods/reactNative.ts +++ b/src/api/methods/reactNative.ts @@ -1,5 +1,5 @@ import { talsecStart } from './native'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { setRaspExecutionStateListener, removeRaspExecutionStateEventListener, @@ -9,38 +9,66 @@ import type { ThreatEventActions, TalsecConfig, RaspExecutionStateEventActions, + FreeRaspStatus, } from '../../types/types'; -import { onInvalidCallback } from './native'; +import { TalsecError } from '../../types/errors'; let isRaspStarted = false; +const toTalsecError = (e: unknown): TalsecError => + e instanceof TalsecError + ? e + : new TalsecError( + 'talsec-failure', + e instanceof Error ? e.message : String(e) + ); + +/** + * Registers the threat and RASP execution state listeners and starts freeRASP. + * + * freeRASP is started only once per app run; later mounts only re-register + * the listeners. + * + * @returns `started` once freeRASP runs, or the `TalsecError` that prevented + * the listeners from being registered or freeRASP from starting. + */ export const useFreeRasp = ( config: TalsecConfig, actions: ThreatEventActions, raspExecutionStateActions?: RaspExecutionStateEventActions -) => { +): FreeRaspStatus => { + const [status, setStatus] = useState({ + started: isRaspStarted, + error: null, + }); + useEffect(() => { + let isMounted = true; + (async () => { - await setThreatListeners(actions); - raspExecutionStateActions && - (await setRaspExecutionStateListener(raspExecutionStateActions)); + try { + await setThreatListeners(actions); + if (raspExecutionStateActions) { + await setRaspExecutionStateListener(raspExecutionStateActions); + } - if (isRaspStarted) { - return; - } + if (!isRaspStarted) { + await talsecStart(config); + isRaspStarted = true; + } - try { - const response = await talsecStart(config); - if (response !== 'freeRASP started') { - onInvalidCallback(); + if (isMounted) { + setStatus({ started: true, error: null }); + } + } catch (e) { + if (isMounted) { + setStatus({ started: isRaspStarted, error: toTalsecError(e) }); } - isRaspStarted = true; - } catch (e: any) { - console.error(`${e.code}: ${e.message}`); } })(); return () => { + isMounted = false; (async () => { await removeThreatListener(); await removeRaspExecutionStateEventListener(); @@ -48,4 +76,6 @@ export const useFreeRasp = ( }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + + return status; }; diff --git a/src/channels/raspExecutionState.ts b/src/channels/raspExecutionState.ts index 7196f20..82a1f51 100644 --- a/src/channels/raspExecutionState.ts +++ b/src/channels/raspExecutionState.ts @@ -1,6 +1,6 @@ import { getRaspExecutionStateCount, itemsHaveType } from '../utils/utils'; import { FreeraspReactNative } from '../api/nativeModules'; -import { onInvalidCallback } from '../api/methods/native'; +import { onInvalidCallback } from '../api/methods/internal'; import { RaspExecutionState } from '../models/raspExecutionState'; export const getRaspExecutionStateIdentifiers = async (): Promise => { diff --git a/src/channels/threat.ts b/src/channels/threat.ts index 9942ac8..8c62230 100644 --- a/src/channels/threat.ts +++ b/src/channels/threat.ts @@ -1,7 +1,7 @@ import { getThreatCount, itemsHaveType } from '../utils/utils'; import { FreeraspReactNative } from '../api/nativeModules'; import { Threat } from '../models/threat'; -import { onInvalidCallback } from '../api/methods/native'; +import { onInvalidCallback } from '../api/methods/internal'; export const getThreatIdentifiers = async (): Promise => { const identifiers = await FreeraspReactNative.getThreatIdentifiers(); diff --git a/src/index.tsx b/src/index.tsx index 70f0246..b063c57 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,5 +1,7 @@ export * from './types/types'; +export { TalsecError, type TalsecErrorCode } from './types/errors'; export * from './api/methods/native'; export * from './api/methods/reactNative'; export * from './api/listeners/threat'; export * from './api/listeners/raspExecutionState'; +export { onInvalidCallback as abortApp } from './api/methods/internal'; diff --git a/src/types/errors.ts b/src/types/errors.ts new file mode 100644 index 0000000..0dd2270 --- /dev/null +++ b/src/types/errors.ts @@ -0,0 +1,57 @@ +/** + * Stable error codes shared with the other freeRASP plugins. + * + * - `talsec-failure` - a native call failed + * - `unsupported-platform` - the feature is not available on this platform + * - `configuration-exception` - the configuration is invalid + * - `external-id-failure` - storing or removing the external ID failed + * - `malware-failure` - a malware-detection call failed + */ +export type TalsecErrorCode = + | 'talsec-failure' + | 'unsupported-platform' + | 'configuration-exception' + | 'external-id-failure' + | 'malware-failure'; + +/** + * The only error freeRASP methods reject with. + */ +export class TalsecError extends Error { + readonly code: TalsecErrorCode; + + constructor(code: TalsecErrorCode, message?: string) { + super(message); + this.name = 'TalsecError'; + this.code = code; + Object.setPrototypeOf(this, TalsecError.prototype); + } +} + +/** + * Runs a native call and rethrows any failure as a {@link TalsecError}. + */ +export const wrapNative = async ( + code: TalsecErrorCode, + fn: () => Promise +): Promise => { + try { + return await fn(); + } catch (e: any) { + if (e instanceof TalsecError) { + throw e; + } + throw new TalsecError(code, e?.message ?? String(e)); + } +}; + +/** + * Rejects with an `unsupported-platform` error for Android-only features. + */ +export const androidOnly = (feature: string): Promise => + Promise.reject( + new TalsecError( + 'unsupported-platform', + `${feature} is not available on iOS` + ) + ); diff --git a/src/types/types.ts b/src/types/types.ts index ddda6d8..a0c8d51 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -1,3 +1,5 @@ +import type { TalsecError } from './errors'; + export type TalsecConfig = { androidConfig?: TalsecAndroidConfig; iosConfig?: TalsecIosConfig; @@ -37,7 +39,7 @@ export type TalsecAndroidConfig = { }; export type TalsecIosConfig = { - appBundleId: string; + appBundleIds: string[]; appTeamId: string; }; @@ -65,33 +67,33 @@ export type PackageInfo = { export type CallState = 'STARTED' | 'ONGOING' | 'STOPPED'; export type ThreatEventActions = { - privilegedAccess?: () => any; - debug?: () => any; - simulator?: () => any; - appIntegrity?: () => any; - unofficialStore?: () => any; - hooks?: () => any; - deviceBinding?: () => any; - deviceID?: () => any; - passcode?: () => any; - secureHardwareNotAvailable?: () => any; - obfuscationIssues?: () => any; - devMode?: () => any; - systemVPN?: () => any; - malware?: (suspiciousApps: SuspiciousAppInfo[]) => any; - adbEnabled?: () => any; - screenshot?: () => any; - screenRecording?: () => any; - multiInstance?: () => any; - timeSpoofing?: () => any; - locationSpoofing?: () => any; - unsecureWifi?: () => any; - automation?: () => any; - bootloader?: () => any; + privilegedAccess?: () => void; + debug?: () => void; + simulator?: () => void; + appIntegrity?: () => void; + unofficialStore?: () => void; + hooks?: () => void; + deviceBinding?: () => void; + deviceID?: () => void; + passcode?: () => void; + secureHardwareNotAvailable?: () => void; + obfuscationIssues?: () => void; + devMode?: () => void; + systemVPN?: () => void; + malware?: (suspiciousApps: SuspiciousAppInfo[]) => void; + adbEnabled?: () => void; + screenshot?: () => void; + screenRecording?: () => void; + multiInstance?: () => void; + timeSpoofing?: () => void; + locationSpoofing?: () => void; + unsecureWifi?: () => void; + automation?: () => void; + bootloader?: () => void; /** * Android only. Requires `startOngoingCallDetection()`. */ - ongoingCall?: (state: CallState) => any; + ongoingCall?: (state: CallState) => void; }; export type NativeEvent = { @@ -104,15 +106,25 @@ export type RaspExecutionStateEventActions = { * checks may still run afterwards. On iOS, triggered when all checks have * been completed. */ - fastChecksFinished?: () => any; + fastChecksFinished?: () => void; /** * Android only. The critical checks have been completed, before the * non-critical checks. */ - criticalChecksFinished?: () => any; + criticalChecksFinished?: () => void; /** * Android only. The postponed checks have been completed, which ends the * initial evaluation. */ - postponedChecksFinished?: () => any; + postponedChecksFinished?: () => void; +}; + +/** + * State returned by `useFreeRasp`. + */ +export type FreeRaspStatus = { + /** `true` once freeRASP has been started. */ + started: boolean; + /** Error that prevented the listeners from being registered or freeRASP from starting. */ + error: TalsecError | null; };