diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f891bc..e36f2ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,55 @@ 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).
+## [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
+- `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)
+- `criticalChecksFinished` and `postponedChecksFinished` RASP execution state actions (Android only)
+
+#### 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
+- Threat and RASP execution state actions are typed as returning `void` instead of `any`
+
+### Android
+
+#### 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
+
+- 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
+- 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..47ac081 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"
}
@@ -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 64%
rename from android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt
rename to android/src/main/java/app/talsec/rasp/reactnative/FreeraspReactNativeModule.kt
index dc71092..2ff64f6 100644
--- a/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt
+++ b/android/src/main/java/app/talsec/rasp/reactnative/FreeraspReactNativeModule.kt
@@ -1,5 +1,7 @@
-package com.freeraspreactnative
+package app.talsec.rasp.reactnative
+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
@@ -20,20 +23,21 @@ 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) :
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/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 79%
rename from android/src/main/java/com/freeraspreactnative/PluginThreatHandler.kt
rename to android/src/main/java/app/talsec/rasp/reactnative/PluginThreatHandler.kt
index 3d7c29c..cd96e29 100644
--- a/android/src/main/java/com/freeraspreactnative/PluginThreatHandler.kt
+++ b/android/src/main/java/app/talsec/rasp/reactnative/PluginThreatHandler.kt
@@ -1,12 +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 com.freeraspreactnative.dispatchers.ExecutionStateDispatcher
-import com.freeraspreactnative.dispatchers.ThreatDispatcher
-import com.freeraspreactnative.events.RaspExecutionStateEvent
-import com.freeraspreactnative.events.ThreatEvent
+import app.talsec.rasp.security.api.ThreatListener.CallState
+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 {
@@ -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,8 +110,16 @@ internal object PluginThreatHandler {
}
private val raspExecutionState = object : ThreatListener.RaspExecutionState() {
- override fun onAllChecksFinished() {
- ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.AllChecksFinished)
+ override fun onCriticalChecksFinished() {
+ ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.CriticalChecksFinished)
+ }
+
+ override fun onFastChecksFinished() {
+ ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.FastChecksFinished)
+ }
+
+ override fun onPostponedChecksFinished() {
+ ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.PostponedChecksFinished)
}
}
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 67%
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 d5f8bea..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,13 +1,15 @@
-package com.freeraspreactnative.dispatchers
+package app.talsec.rasp.reactnative.dispatchers
import app.talsec.rasp.security.api.SuspiciousAppInfo
-import com.freeraspreactnative.events.ThreatEvent
-import com.freeraspreactnative.interfaces.PluginThreatListener
+import app.talsec.rasp.security.api.ThreatListener.CallState
+import app.talsec.rasp.reactnative.events.ThreatEvent
+import app.talsec.rasp.reactnative.interfaces.PluginThreatListener
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/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/app/talsec/rasp/reactnative/events/RaspExecutionStateEvent.kt b/android/src/main/java/app/talsec/rasp/reactnative/events/RaspExecutionStateEvent.kt
new file mode 100644
index 0000000..5a9dadf
--- /dev/null
+++ b/android/src/main/java/app/talsec/rasp/reactnative/events/RaspExecutionStateEvent.kt
@@ -0,0 +1,22 @@
+package app.talsec.rasp.reactnative.events
+
+import app.talsec.rasp.reactnative.utils.RandomGenerator
+
+internal sealed class RaspExecutionStateEvent(override val value: Int) : BaseRaspEvent {
+ override val channelName: String get() = CHANNEL_NAME
+ override val channelKey: String get() = CHANNEL_KEY
+
+ data object FastChecksFinished : 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(
+ FastChecksFinished,
+ CriticalChecksFinished,
+ PostponedChecksFinished
+ ).map { it.value }
+ }
+}
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 90%
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 7e9fe5e..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.
@@ -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/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 51%
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 cc949b3..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,9 +1,11 @@
-package com.freeraspreactnative.interfaces
+package app.talsec.rasp.reactnative.interfaces
import app.talsec.rasp.security.api.SuspiciousAppInfo
-import com.freeraspreactnative.events.ThreatEvent
+import app.talsec.rasp.security.api.ThreatListener.CallState
+import app.talsec.rasp.reactnative.events.ThreatEvent
internal interface PluginThreatListener {
fun threatDetected(threatEventType: ThreatEvent)
fun malwareDetected(suspiciousApps: MutableList)
+ fun ongoingCallDetected(state: CallState)
}
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
diff --git a/android/src/main/java/com/freeraspreactnative/events/RaspExecutionStateEvent.kt b/android/src/main/java/com/freeraspreactnative/events/RaspExecutionStateEvent.kt
deleted file mode 100644
index eb9cfc8..0000000
--- a/android/src/main/java/com/freeraspreactnative/events/RaspExecutionStateEvent.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.freeraspreactnative.events
-
-import com.freeraspreactnative.utils.RandomGenerator
-
-internal sealed class RaspExecutionStateEvent(override val value: Int) : BaseRaspEvent {
- override val channelName: String get() = CHANNEL_NAME
- override val channelKey: String get() = CHANNEL_KEY
-
- data object AllChecksFinished : 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
- ).map { it.value }
- }
-}
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..78661a5 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';
@@ -11,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,
@@ -22,10 +27,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();
})();
}, []);
@@ -55,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',
@@ -277,13 +289,32 @@ 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',
+ detail: callStateLabel(state),
+ }
+ : threat
+ )
+ );
+ },
};
const raspExecutionStateActions = {
// Android & iOS
- allChecksFinished: () => {
+ fastChecksFinished: () => {
setAllChecksStatus('completed');
},
+ // Android only
+ postponedChecksFinished: () => {
+ setPostponedChecksStatus('completed');
+ },
};
const requestLocationPermission = async () => {
@@ -320,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);
}
@@ -331,13 +360,26 @@ 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 (
);
};
diff --git a/example/src/DemoApp.tsx b/example/src/DemoApp.tsx
index 1e72cb1..3e70a4a 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,
@@ -33,12 +38,23 @@ export const DemoApp: React.FC<{
checks: {
name: string;
status: string;
+ detail?: string;
}[];
suspiciousApps: SuspiciousAppInfo[];
- allChecksFinishedStatus: 'in progress' | 'completed';
-}> = ({ checks, suspiciousApps, allChecksFinishedStatus }) => {
+ fastChecksFinishedStatus: 'in progress' | 'completed';
+ postponedChecksFinishedStatus: 'in progress' | 'completed';
+}> = ({
+ checks,
+ suspiciousApps,
+ fastChecksFinishedStatus,
+ 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);
@@ -59,14 +75,82 @@ export const DemoApp: React.FC<{
const toggleScreenCaptureBlock = async () => {
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);
}
};
+ 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(
+ 'Fast Checks Finished',
+ fastChecksFinishedStatus
+ )}
+ {Platform.OS === 'android' &&
+ renderExecutionState(
+ 'Postponed Checks Finished',
+ postponedChecksFinishedStatus
+ )}
freeRASP checks:
{checks.map((check: any, idx: number) => (
- {check.name}
+ {check.detail
+ ? `${check.name}: ${check.detail}`
+ : check.name}
{check.name === 'Malware' && (
= ({ 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/example/src/checks.ts b/example/src/checks.ts
index 1b777d0..c64f78d 100644
--- a/example/src/checks.ts
+++ b/example/src/checks.ts
@@ -26,4 +26,5 @@ export const androidChecks = [
{ name: 'Unsecure Wifi', status: 'ok' },
{ name: 'Automation', status: 'ok' },
{ name: 'Bootloader', status: 'ok' },
+ { name: 'Ongoing Call', status: 'ok', detail: 'Not detected' },
];
diff --git a/ios/FreeraspReactNative.swift b/ios/FreeraspReactNative.swift
index 740ad50..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)
}
@@ -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"])
}
/**
@@ -172,8 +173,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 b61df6a..6128c77 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "freerasp-react-native",
- "version": "5.3.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 c39ea18..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,
@@ -47,8 +47,14 @@ 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?.();
+ break;
+ case RaspExecutionState.PostponedChecksFinished.value:
+ config.postponedChecksFinished?.();
break;
default:
onInvalidCallback();
diff --git a/src/api/listeners/threat.ts b/src/api/listeners/threat.ts
index be4d106..99b1e6b 100644
--- a/src/api/listeners/threat.ts
+++ b/src/api/listeners/threat.ts
@@ -1,8 +1,12 @@
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 { NativeEvent, ThreatEventActions } from '../../types/types';
+import type {
+ CallState,
+ NativeEvent,
+ ThreatEventActions,
+} from '../../types/types';
import { parseMalwareData } from '../../utils/malware';
import {
getThreatChannelData,
@@ -15,6 +19,12 @@ let eventsListener: EmitterSubscription | null = null;
let threatChannel: string | null = null;
let threatKey: string | null = null;
let threatMalwareKey: string | null = null;
+let threatCallStateKey: string | null = null;
+
+const callStates: readonly string[] = ['STARTED', 'ONGOING', 'STOPPED'];
+
+const isCallState = (value: unknown): value is CallState =>
+ typeof value === 'string' && callStates.includes(value);
let isMappingPrepared = false;
let isInitializing = false;
@@ -28,8 +38,14 @@ export const setThreatListeners = async (config: ThreatEventActions) => {
await removeThreatListener();
- if (!threatChannel || !threatKey || !threatMalwareKey) {
- [threatChannel, threatKey, threatMalwareKey] = await getThreatChannelData();
+ if (
+ !threatChannel ||
+ !threatKey ||
+ !threatMalwareKey ||
+ !threatCallStateKey
+ ) {
+ [threatChannel, threatKey, threatMalwareKey, threatCallStateKey] =
+ await getThreatChannelData();
}
if (!isMappingPrepared) {
@@ -38,7 +54,7 @@ export const setThreatListeners = async (config: ThreatEventActions) => {
}
const listener = async (event: NativeEvent) => {
- if (!threatKey || !threatMalwareKey) {
+ if (!threatKey || !threatMalwareKey || !threatCallStateKey) {
onInvalidCallback();
return;
}
@@ -116,6 +132,14 @@ export const setThreatListeners = async (config: ThreatEventActions) => {
case Threat.Bootloader.value:
config.bootloader?.();
break;
+ case Threat.OngoingCall.value:
+ const callState = event[threatCallStateKey];
+ if (!isCallState(callState)) {
+ onInvalidCallback();
+ break;
+ }
+ config.ongoingCall?.(callState);
+ break;
default:
onInvalidCallback();
break;
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 cc3d089..ef77da3 100644
--- a/src/api/methods/native.ts
+++ b/src/api/methods/native.ts
@@ -1,48 +1,152 @@
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()
+ );
};
-export const storeExternalId = (data: string): Promise => {
- return FreeraspReactNative.storeExternalId(data);
+/**
+ * Android only. Prevents accessibility services from reading or controlling
+ * the app screen.
+ */
+export const blockScreenControl = async (enable: boolean): Promise => {
+ if (Platform.OS === 'ios') {
+ return androidOnly('Screen control blocking');
+ }
+ await wrapNative('talsec-failure', () =>
+ FreeraspReactNative.blockScreenControl(enable)
+ );
};
-export const removeExternalId = (): Promise => {
- return FreeraspReactNative.removeExternalId();
+/**
+ * Android only. Returns whether screen control is blocked.
+ */
+export const isScreenControlBlocked = (): Promise => {
+ if (Platform.OS === 'ios') {
+ return androidOnly('Screen control blocking');
+ }
+ return wrapNative('talsec-failure', () =>
+ FreeraspReactNative.isScreenControlBlocked()
+ );
};
-export const getAppIcon = (packageName: string): Promise => {
+/**
+ * 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 wrapNative('talsec-failure', () =>
+ 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 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 = async (): Promise => {
+ if (Platform.OS === 'ios') {
+ return androidOnly('Ongoing call detection');
+ }
+ await wrapNative('talsec-failure', () =>
+ FreeraspReactNative.startOngoingCallDetection()
+ );
+};
+
+/**
+ * Android only. Stops the ongoing call detection.
+ */
+export const stopOngoingCallDetection = async (): Promise => {
if (Platform.OS === 'ios') {
- return Promise.reject(
- 'App icon retrieval for Malware detection not available on iOS'
- );
+ return androidOnly('Ongoing call detection');
}
- return FreeraspReactNative.getAppIcon(packageName);
+ await wrapNative('talsec-failure', () =>
+ FreeraspReactNative.stopOngoingCallDetection()
+ );
};
-export const onInvalidCallback = (): void => {
- FreeraspReactNative.onInvalidCallback();
+/**
+ * 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 removeListenerForEvent = (channel: string): Promise => {
- return FreeraspReactNative.removeListenerForEvent(channel);
+/**
+ * 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 androidOnly('App icon retrieval for malware detection');
+ }
+ 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 f63c8ba..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();
@@ -15,10 +15,10 @@ export const getThreatIdentifiers = async (): Promise => {
};
export const getThreatChannelData = async (): Promise<
- [string, string, string]
+ [string, string, string, string]
> => {
const data = await FreeraspReactNative.getThreatChannelData();
- if (data.length !== 3 || !itemsHaveType(data, 'string')) {
+ if (data.length !== 4 || !itemsHaveType(data, 'string')) {
onInvalidCallback();
}
return data;
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/models/raspExecutionState.ts b/src/models/raspExecutionState.ts
index a46c800..3c60fd3 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 FastChecksFinished = 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.FastChecksFinished,
+ this.CriticalChecksFinished,
+ this.PostponedChecksFinished,
+ ]
+ : [this.FastChecksFinished];
}
}
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/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 93b1edf..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;
};
@@ -55,34 +57,74 @@ 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;
- 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) => void;
};
-export type NativeEvent = { [key: string]: number | string[] | undefined };
+export type NativeEvent = {
+ [key: string]: number | string | string[] | undefined;
+};
export type RaspExecutionStateEventActions = {
- allChecksFinished?: () => any;
+ /**
+ * 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.
+ */
+ fastChecksFinished?: () => void;
+ /**
+ * Android only. The critical checks have been completed, before the
+ * non-critical checks.
+ */
+ criticalChecksFinished?: () => void;
+ /**
+ * Android only. The postponed checks have been completed, which ends the
+ * initial evaluation.
+ */
+ 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;
};