From c3124e822f4a7dbbbcfab17dd5824e1cac1d5f9d Mon Sep 17 00:00:00 2001 From: Tomas Kypta Date: Tue, 21 May 2024 22:03:20 +0200 Subject: [PATCH 1/6] Update Malwarelytics for Apple to 2.1.1 --- .../malwarelytics/reactnative/AVObjects.kt | 5 +- example/ios/MalwarelyticsExample/Info.plist | 12 ++++ example/ios/Podfile.lock | 2 +- example/src/App.tsx | 2 +- example/src/Config.ts | 13 +++- ios/Malwarelytics+RASP.swift | 47 +++++++++++++ ios/MalwarelyticsConfig.swift | 69 ++++++++++++++++++- ios/RNHelper.swift | 10 +++ react-native-malwarelytics.podspec | 2 +- src/MalwarelyticsConfig.ts | 62 ++++++++++++++++- src/MalwarelyticsRasp.ts | 20 +++--- src/internal/RaspEvent.ts | 2 +- src/model/rasp/AppPresenceInfo.ts | 20 ++++++ 13 files changed, 248 insertions(+), 18 deletions(-) diff --git a/android/src/main/java/com/wultra/android/malwarelytics/reactnative/AVObjects.kt b/android/src/main/java/com/wultra/android/malwarelytics/reactnative/AVObjects.kt index abb4605..aca5a0a 100644 --- a/android/src/main/java/com/wultra/android/malwarelytics/reactnative/AVObjects.kt +++ b/android/src/main/java/com/wultra/android/malwarelytics/reactnative/AVObjects.kt @@ -105,9 +105,12 @@ fun ActiveCallDetection.toJs(): ReadableMap { } fun AppPresenceDetection.toJs(): ReadableMap { + // AndroidAppPresenceInfo + val androidAppPresenceInfo = Arguments.createMap() + .put("remoteDesktopApps", remoteDesktopApps.toJs { it.toJs() }) // AppPresenceInfo return Arguments.createMap() - .put("remoteDesktopApps", remoteDesktopApps.toJs { it.toJs() }) + .put("androidAppPresenceInfo", androidAppPresenceInfo) } fun NamedApkItemInfo.toJs(): ReadableMap { diff --git a/example/ios/MalwarelyticsExample/Info.plist b/example/ios/MalwarelyticsExample/Info.plist index d2e7883..3a9ae43 100644 --- a/example/ios/MalwarelyticsExample/Info.plist +++ b/example/ios/MalwarelyticsExample/Info.plist @@ -51,5 +51,17 @@ UIViewControllerBasedStatusBarAppearance + LSApplicationQueriesSchemes + + cydia + anydesk + tvsqcustomer1 + logmein + rdp + jump + prlclient + tuxclient + crd + diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index da82183..07c5492 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1384,7 +1384,7 @@ SPEC CHECKSUMS: React-logger: 7e7403a2b14c97f847d90763af76b84b152b6fce React-Mapbuffer: 11029dcd47c5c9e057a4092ab9c2a8d10a496a33 react-native-config: 86038147314e2e6d10ea9972022aa171e6b1d4d8 - react-native-malwarelytics: 9435d99f3a73dc5754c4d7f50417f387fae4db75 + react-native-malwarelytics: 847ebaa9964fb1373d3915b62c6ccaa97b7ddb1a React-nativeconfig: b0073a590774e8b35192fead188a36d1dca23dec React-NativeModulesApple: df46ff3e3de5b842b30b4ca8a6caae6d7c8ab09f React-perflogger: 3d31e0d1e8ad891e43a09ac70b7b17a79773003a diff --git a/example/src/App.tsx b/example/src/App.tsx index 557cdc1..f6d03e9 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -192,6 +192,7 @@ class App extends React.Component<{}, AppState> implements MalwarelyticsStateLis params.push({key: 'Screen Sharing', value: JSON.stringify(await rasp.getScreenSharingInfo())}) params.push({key: 'VPN', value: `${await rasp.isVpnActive()}`}) params.push({key: 'On Call', value: `${await rasp.isOnCall()}`}) + params.push({key: 'App Presence', value: JSON.stringify(await rasp.getAppPresenceInfo())}) if (Platform.OS == 'ios') { // iOS specific @@ -210,7 +211,6 @@ class App extends React.Component<{}, AppState> implements MalwarelyticsStateLis params.push({key: 'Tapjacking', value: JSON.stringify(await rasp.getTapjackingInfo())}) params.push({key: 'Bad Tapjacking App', value: `${await rasp.isBadTapjackingCapableAppPresent()}`}) params.push({key: 'Active Call', value: JSON.stringify(await rasp.getActiveCallInfo())}) - params.push({key: 'App Presence', value: JSON.stringify(await rasp.getAppPresenceInfo())}) } } catch (error) { params.push({key: '💣 Exception', value: ErrorToString(error)}) diff --git a/example/src/Config.ts b/example/src/Config.ts index 852bd4b..0b327e0 100644 --- a/example/src/Config.ts +++ b/example/src/Config.ts @@ -40,7 +40,18 @@ const defaultConfig: MalwarelyticsConfig = { // signaturePublicKey: "your-sign-pk-for-ios-app" // }, rasp: { - repackage: { action: "NOTIFY" } + repackage: { action: "NOTIFY" }, + appPresence: { + action: "NOTIFY", + apps: [ + { + deeplinkProtocols: ["anydesk"], + name: "AnyDesk", + category: "REMOTE_DESKTOP", + tag: "anydesk" + } + ] + } } }, android: { diff --git a/ios/Malwarelytics+RASP.swift b/ios/Malwarelytics+RASP.swift index ce0301a..b443b31 100644 --- a/ios/Malwarelytics+RASP.swift +++ b/ios/Malwarelytics+RASP.swift @@ -61,6 +61,14 @@ extension Malwarelytics: AppProtectionRaspDelegate { func onCallChanged(isOnCall: Bool) { sendRaspEvent(RASPMessage(type: "ON_CALL", payload: isOnCall)) } + + func installedAppsChanged(installedApps: [DetectableApp]) { + sendRaspEvent(RASPMessage(type: "APP_PRESENCE", + payload: AppPresenceInfo( + appleAppPresenceInfo: AppleAppPresenceInfo(installedApps: installedApps) + ) + )) + } /// Send RASP event to JavaScript. /// - Parameter payload: Message body. @@ -101,6 +109,10 @@ extension Malwarelytics { return rasp.isVpnActive case "ON_CALL": return rasp.isOnCall + case "APP_PRESENCE": + return AppPresenceInfo( + appleAppPresenceInfo: AppleAppPresenceInfo(installedApps: rasp.installedApps) + ) default: throw ModuleError.invalidParam(paramName: "message") } @@ -141,3 +153,38 @@ fileprivate struct EmulatorInfo: Encodable { let isEmulator: Bool let detectedEmulatorType: String } + +fileprivate struct AppPresenceInfo: Encodable { + let appleAppPresenceInfo: AppleAppPresenceInfo? +} + +fileprivate struct AppleAppPresenceInfo: Encodable { + let installedApps: [DetectableApp] +} + +extension DetectableApp : Encodable { + enum CodingKeys: CodingKey { + case deeplinkProtocols + case name + case category + case tag + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(deeplinkProtocols, forKey: .deeplinkProtocols) + try container.encode(name, forKey: .name) + try container.encode(category, forKey: .category) + try container.encode(tag, forKey: .tag) + } +} + +extension DetectableApp.Category : Encodable { + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .remoteDesktop: + try container.encode("REMOTE_DESKTOP") + } + } +} \ No newline at end of file diff --git a/ios/MalwarelyticsConfig.swift b/ios/MalwarelyticsConfig.swift index 1e10e3d..74c9023 100644 --- a/ios/MalwarelyticsConfig.swift +++ b/ios/MalwarelyticsConfig.swift @@ -97,7 +97,9 @@ extension AppProtectionRaspConfig { httpProxy: try .fromDictionary(dict.dictOptAt("httpProxy")) ?? fallback.httpProxy, repackage: try .fromDictionary(dict.dictOptAt("repackage")) ?? fallback.repackage, screenCapture: try .fromDictionary(dict.dictOptAt("screenCapture")) ?? fallback.screenCapture, - vpnDetection: try .fromDictionary(dict.dictOptAt("vpn")) ?? fallback.vpnDetection + vpnDetection: try .fromDictionary(dict.dictOptAt("vpn")) ?? fallback.vpnDetection, + callDetection: try .fromDictionary(dict.dictOptAt("call")) ?? fallback.callDetection, + appPresence: try .fromDictionary(dict.dictOptAt("appPresence")) ?? fallback.appPresence ) } @@ -218,6 +220,71 @@ extension AppProtectionRaspConfig.RepackageConfig { static let jsObj = "MalwarelyticsAppleRepackagingDetectionConfig" } +extension AppProtectionRaspConfig.SimpleDetectionConfig { + /// Create AppProtectionRaspConfig.SimpleDetectionConfig from JSON dictionary. + /// - Parameter dict: Dictionary with configuration. + /// - Returns: Configuration created from JSON dictionary. + static func fromDictionary(_ dict: ConfigDictionary?) throws -> AppProtectionRaspConfig.SimpleDetectionConfig? { + guard let dict = dict else { + return nil + } + let action = try dict.stringAt("action", in: jsObj) + switch action { + case "NO_ACTION": + return .noAction + case "NOTIFY": + return .notify + default: + throw ModuleError.invalidConfig(configPath: "action", object: jsObj) + } + } + + /// Name of configuration object repodted in the exception. + static let jsObj = "MalwarelyticsAppleSimpleDetectionConfig" +} + +extension AppProtectionRaspConfig.AppPresenceDetectionConfig { + /// Create AppProtectionRaspConfig.AppPresenceDetectionConfig from JSON dictionary. + /// - Parameter dict: Dictionary with configuration. + /// - Returns: Configuration created from JSON dictionary. + static func fromDictionary(_ dict: ConfigDictionary?) throws -> AppProtectionRaspConfig.AppPresenceDetectionConfig? { + guard let dict = dict else { + return nil + } + let action = try dict.stringAt("action", in: jsObj) + let srcApps = try dict.dictArrayAt("apps", in: jsObj) + let apps = try srcApps.map { + let dlProtocols = try $0.stringArrayAt("deeplinkProtocols", in: jsObj) + let name = try $0.stringAt("name", in: jsObj) + let cat = try $0.stringAt("category", in: jsObj) + let category = switch cat { + case "REMOTE_DESKTOP": DetectableApp.Category.remoteDesktop + default: + throw ModuleError.invalidConfig(configPath: "category", object: jsObj) + } + let tag = try $0.stringAt("tag", in: jsObj) + return DetectableApp( + deeplinkProtocols: dlProtocols, + name: name, + category: category, + tag: tag + ) + } + + switch action { + case "MANUAL": + return .manual(apps) + case "NOTIFY": + return .notify(apps) + default: + throw ModuleError.invalidConfig(configPath: "action", object: jsObj) + } + } + + /// Name of configuration object repodted in the exception. + static let jsObj = "MalwarelyticsAppleAppPresenceDetectionConfig" +} + extension AppProtectionEventConfig { /// Create AppProtectionEventConfig from JSON dictionary. /// - Parameter dict: Dictionary with configuration. diff --git a/ios/RNHelper.swift b/ios/RNHelper.swift index 1a2bd85..30aa140 100644 --- a/ios/RNHelper.swift +++ b/ios/RNHelper.swift @@ -107,6 +107,16 @@ extension ConfigDictionary { } return v } + + func dictArrayAt(_ path: String, in object: String? = nil, required: Bool = false, fallback: [ConfigDictionary] = []) throws -> [ConfigDictionary] { + guard let v = try objectAt(path: path, object: object, required: required) as? Array else { + if required { + throw ModuleError.missingConfig(configPath: path, object: object) + } + return fallback + } + return v + } func dictOptAt(_ path: String, in object: String? = nil) throws -> ConfigDictionary? { return try? objectAt(path: path, object: object) as? ConfigDictionary diff --git a/react-native-malwarelytics.podspec b/react-native-malwarelytics.podspec index aa73a14..0d12a23 100644 --- a/react-native-malwarelytics.podspec +++ b/react-native-malwarelytics.podspec @@ -34,7 +34,7 @@ Pod::Spec.new do |s| # Integrate Malwarelytics for iOS. s.prepare_command = <<-CMD - ./ios-native/prepare.sh 2.0.0 + ./ios-native/prepare.sh 2.1.1 CMD s.vendored_frameworks = "ios-native/AppProtection.xcframework" diff --git a/src/MalwarelyticsConfig.ts b/src/MalwarelyticsConfig.ts index 1b4fc0d..c286bb9 100644 --- a/src/MalwarelyticsConfig.ts +++ b/src/MalwarelyticsConfig.ts @@ -152,9 +152,18 @@ export interface MalwarelyticsAppleRaspConfig { */ screenCapture?: MalwarelyticsAppleBasicDetectionConfig; /** - * VPN detection config. + * VPN detection config. The default value is `NOTIFY`. */ vpn?: MalwarelyticsAppleBasicDetectionConfig; + /** + * Call detection config. The default value is `NOTIFY`. + */ + call?: MalwarelyticsAppleSimpleDetectionConfig; + /** + * App presence detection. 3rd party app presence. + * The default value is `.manual` with empty array of apps. + */ + appPresence?: MalwarelyticsAppleAppPresenceDetectionConfig; } /** @@ -258,6 +267,45 @@ export interface MalwarelyticsAppleDebuggerDetectionConfig { exitUrl?: string; } +/** + * Configuration of the simple detection behavior. + */ +export interface MalwarelyticsAppleSimpleDetectionConfig { + /** + * Behavior of the detection + */ + action: MalwarelyticsAppleSimpleDetectionAction; +} + +/** + * Configuration of the app presence detection behavior. + */ +export interface MalwarelyticsAppleAppPresenceDetectionConfig { + /** + * Behavior of the detection + */ + action: MalwarelyticsAppleAppPresenceDetectionAction; + /** + * Applications that can be detected on the phone if present. + */ + apps: MalwarelyticsAppleDetectableApp[]; +} + +/** + * Configuration of application that can be detected on the phone if present. + */ +export interface MalwarelyticsAppleDetectableApp { + deeplinkProtocols: string[]; + name: string; + category: MalwarelyticsAppleDetectableAppCategory; + tag?: string; +} + +/** + * Category of MalwarelyticsAppleDetectableApp + * REMOTE_DESKTOP - Remote desktop apps are apps that can screen cast phone screen. + */ +export type MalwarelyticsAppleDetectableAppCategory = "REMOTE_DESKTOP" /** * NO_ACTION - do nothing @@ -274,6 +322,18 @@ export type MalwarelyticsAppleDebuggerDetectionAction = "NO_ACTION" | "NOTIFY" | */ export type MalwarelyticsAppleDetectionAction = "NO_ACTION" | "NOTIFY" | "EXIT"; +/** + * NO_ACTION - do nothing + * NOTIFY - notify via the observer + */ +export type MalwarelyticsAppleSimpleDetectionAction = "NO_ACTION" | "NOTIFY"; + +/** + * MANUAL - automatic detection is turned off, you can do a manual check + * NOTIFY - notify via the observer + */ +export type MalwarelyticsAppleAppPresenceDetectionAction = "MANUAL" | "NOTIFY" + // --------------------------------------------------------------------------------------- diff --git a/src/MalwarelyticsRasp.ts b/src/MalwarelyticsRasp.ts index 96ce636..aedc047 100644 --- a/src/MalwarelyticsRasp.ts +++ b/src/MalwarelyticsRasp.ts @@ -89,6 +89,9 @@ export class MalwarelyticsRasp { case "VPN": listener.vpnDetected(m.payload as boolean) break + case "APP_PRESENCE": + listener.appPresenceChangeDetected(m.payload as AppPresenceInfo) + break; // Android specific @@ -102,9 +105,7 @@ export class MalwarelyticsRasp { case "ACTIVE_CALL": listener.activeCallDetected(m.payload as ActiveCallInfo) break; - case "APP_PRESENCE": - listener.appPresenceChangeDetected(m.payload as AppPresenceInfo) - break; + // Apple specific @@ -202,6 +203,12 @@ export class MalwarelyticsRasp { isOnCall(): Promise { return this.getRaspInfo("ON_CALL") } + /** + * Obtain information about app presence. + */ + getAppPresenceInfo(): Promise { + return this.getRaspInfo("APP_PRESENCE") + } // Apple specific @@ -291,13 +298,6 @@ export class MalwarelyticsRasp { getActiveCallInfo(): Promise { return this.getRaspAndroidInfo("ACTIVE_CALL") } - - /** - * Android specific: Obtain information about app presence. - */ - getAppPresenceInfo(): Promise { - return this.getRaspAndroidInfo("APP_PRESENCE") - } // Private methods diff --git a/src/internal/RaspEvent.ts b/src/internal/RaspEvent.ts index f161698..a3946a5 100644 --- a/src/internal/RaspEvent.ts +++ b/src/internal/RaspEvent.ts @@ -27,6 +27,7 @@ export type RaspEventType = // Platforms | Payload "EMULATOR" | // Android Apple EmulatorInfo "VPN" | // Android Apple boolean "ON_CALL" | // Android Apple boolean + "APP_PRESENCE" | // Android Apple AppPresenceInfo "TAPJACKING" | // Android TapjackingInfo "ADB_STATUS" | // Android boolean "SCREEN_LOCK" | // Android boolean @@ -36,7 +37,6 @@ export type RaspEventType = // Platforms | Payload "DEVELOPER_MODE" | // Android boolean "BIOMETRY" | // Android BiometryInfo "ACTIVE_CALL" | // Android ActiveCallInfo - "APP_PRESENCE" | // Android AppPresenceInfo "DEBUGGER_INFO" | // Android DebuggerInfo "SCREENSHOT" | // Apple "REVERSE_TOOLS" | // Apple diff --git a/src/model/rasp/AppPresenceInfo.ts b/src/model/rasp/AppPresenceInfo.ts index d514432..f36ae01 100644 --- a/src/model/rasp/AppPresenceInfo.ts +++ b/src/model/rasp/AppPresenceInfo.ts @@ -1,4 +1,19 @@ +import { MalwarelyticsAppleDetectableApp } from '../../MalwarelyticsConfig' + +/** App presence detection data. */ export interface AppPresenceInfo { + /** + * Android + */ + readonly androidAppPresenceInfo?: AndroidAppPresenceinfo; + /** + * Apple + */ + readonly appleAppPresenceInfo?: AppleAppPresenceInfo; +} + +/** Android app presence detection data. */ +export interface AndroidAppPresenceinfo { readonly remoteDesktopApps: [NamedApkItemInfo]; } @@ -16,4 +31,9 @@ export interface NamedApkItemInfo { readonly versionCode: number; /** Base64 encoded SHA-1 signature hash. */ readonly signatureHash: string; +} + +/** Apple app presence detection data. */ +export interface AppleAppPresenceInfo { + readonly installedApps: [MalwarelyticsAppleDetectableApp]; } \ No newline at end of file From 7346bc6f52a85737b8581f1c9ebe1cef500118f6 Mon Sep 17 00:00:00 2001 From: Tomas Kypta Date: Wed, 22 May 2024 15:58:26 +0200 Subject: [PATCH 2/6] Pass DetectableApps data from iOS to RN --- example/src/App.tsx | 4 ++-- example/src/Config.ts | 28 ++++++++++++++++++---------- ios/Malwarelytics-Bridge.m | 1 + ios/Malwarelytics.swift | 18 ++++++++++++++++++ src/Malwarelytics.ts | 15 +++++++++++++++ src/MalwarelyticsConfig.ts | 3 +++ src/internal/MalwarelyticsModule.ts | 4 +++- 7 files changed, 60 insertions(+), 13 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index f6d03e9..92ec04e 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -18,7 +18,7 @@ import * as React from 'react'; import { StyleSheet, View, Text, Appearance, NativeEventSubscription, SafeAreaView, Button, Platform, SectionList } from 'react-native'; import { EmulatorInfo, HttpProxyInfo, Malwarelytics, MalwarelyticsError, MalwarelyticsRaspListener, MalwarelyticsState, MalwarelyticsStateListener, RepackagingInfo, ScreenSharingInfo, SystemIntegrityInfo, TapjackingInfo, ActiveCallInfo, AppPresenceInfo } from 'react-native-malwarelytics'; -import { loadMalwarelyticsConfig } from './Config'; +import { loadMalwarelyticsConfigAsync } from './Config'; function AppLog(message: string) { console.log(`${Platform.OS}: ${message}`) @@ -113,7 +113,7 @@ class App extends React.Component<{}, AppState> implements MalwarelyticsStateLis return } AppLog("Going to initialize..") - const result = await this.service.initialize(loadMalwarelyticsConfig()) + const result = await this.service.initialize(await loadMalwarelyticsConfigAsync()) AppLog(`Module is now initialized with result ${result}`) } catch (error) { AppErr(`Exception while init': ${ErrorToString(error)}`) diff --git a/example/src/Config.ts b/example/src/Config.ts index 0b327e0..75c49c1 100644 --- a/example/src/Config.ts +++ b/example/src/Config.ts @@ -14,8 +14,9 @@ // and limitations under the License. // -import { MalwarelyticsConfig, MalwarelyticsServiceConfig, MalwarelyticsServiceEnvironment } from "react-native-malwarelytics" +import { MalwarelyticsAppleDetectableApp, MalwarelyticsConfig, MalwarelyticsServiceConfig, MalwarelyticsServiceEnvironment } from "react-native-malwarelytics" import { Config } from "react-native-config" +import { Malwarelytics } from "react-native-malwarelytics" const defaultConfig: MalwarelyticsConfig = { @@ -43,14 +44,7 @@ const defaultConfig: MalwarelyticsConfig = { repackage: { action: "NOTIFY" }, appPresence: { action: "NOTIFY", - apps: [ - { - deeplinkProtocols: ["anydesk"], - name: "AnyDesk", - category: "REMOTE_DESKTOP", - tag: "anydesk" - } - ] + apps: [] // filled in later } } }, @@ -74,12 +68,26 @@ const defaultConfig: MalwarelyticsConfig = { } } +/** + * Load default Malwarelytics config. Include constants loaded from the native SDK. + */ +export async function loadMalwarelyticsConfigAsync(): Promise { + return Malwarelytics.sharedInstance.getKnownDetectableApps() + .catch(() => [] as MalwarelyticsAppleDetectableApp[]) + .then((detectableApps) => { + return loadMalwarelyticsConfig(detectableApps) + }) +} + /** * Load default Malwarelytics config. If `.env` file contains service credentials, then mix * the credentials with the default config. */ -export function loadMalwarelyticsConfig(): MalwarelyticsConfig { +function loadMalwarelyticsConfig(detectableApps: MalwarelyticsAppleDetectableApp[]): MalwarelyticsConfig { var config = {...defaultConfig} + if (config?.apple?.rasp?.appPresence) { + config!.apple!.rasp!.appPresence!.apps = detectableApps + } const envConfig = loadEnvConfig() if (envConfig?.environment != undefined) { config.environment = envConfig.environment diff --git a/ios/Malwarelytics-Bridge.m b/ios/Malwarelytics-Bridge.m index 2d35ba6..fbf3756 100644 --- a/ios/Malwarelytics-Bridge.m +++ b/ios/Malwarelytics-Bridge.m @@ -31,6 +31,7 @@ @interface RCT_EXTERN_MODULE(Malwarelytics, NSObject) RCT_EXTERN_METHOD(setClientId:(NSString*)clientId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(setDeviceId:(NSString*)deviceId resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(getAvUserId:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) +RCT_EXTERN_METHOD(getKnownDetectableApps:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) + (BOOL) requiresMainQueueSetup { diff --git a/ios/Malwarelytics.swift b/ios/Malwarelytics.swift index 6542c81..e4c3df5 100644 --- a/ios/Malwarelytics.swift +++ b/ios/Malwarelytics.swift @@ -156,6 +156,24 @@ class Malwarelytics: RCTEventEmitter { return try StateWithResult(state: self.state.asString, result: self.initResult?.asString).toJsObject() } } + + @objc(getKnownDetectableApps:rejecter:) + func getKnownDetectableApps(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void { + let allKnownApps: [DetectableApp] = [ + .KnownApps.anyDesk, + .KnownApps.teamViewer, + .KnownApps.logMeIn, + .KnownApps.msRemoteDekstop, + .KnownApps.jumpDesktop, + .KnownApps.parallelsAccess, + .KnownApps.chromeRemoteDesktop, + ] + do { + resolve(try allKnownApps.toJsObject()) + } catch { + error.report(to: reject) + } + } @objc(getSupportedEvents:rejecter:) func getSupportedEvents(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void { diff --git a/src/Malwarelytics.ts b/src/Malwarelytics.ts index bde307b..1996d8a 100644 --- a/src/Malwarelytics.ts +++ b/src/Malwarelytics.ts @@ -20,6 +20,9 @@ import { EventHelper } from "./internal/EventHelper"; import type { MalwarelyticsConfig } from "./MalwarelyticsConfig"; import { MalwarelyticsRasp } from "./MalwarelyticsRasp"; import { MalwarelyticsAntivirus } from "./MalwarelyticsAntivirus"; +import { MalwarelyticsAppleDetectableApp } from "./MalwarelyticsConfig"; +import { Platform } from "react-native"; +import { MalwarelyticsError } from "./MalwarelyticsError"; /** * JavaScript wrapper around native kotlin/swift Malwarelytics code. @@ -58,6 +61,18 @@ export class Malwarelytics { }) } + /** + * Apple specific: Obtain list of DetectableApp.KnownApp items that are predefined in native iOS library. + * + * @returns List of DetectableApp.KnownApp items. + */ + getKnownDetectableApps(): Promise { + if (Platform.OS != "ios") { + return Promise.reject(new MalwarelyticsError("METHOD_NOT_SUPPORTED", "This method is supported only on Apple platforms")) + } + return this.withModule(module => module.getKnownDetectableApps()) + } + /** * Initialize Malwarelytics instance with given configuration. * diff --git a/src/MalwarelyticsConfig.ts b/src/MalwarelyticsConfig.ts index c286bb9..a48e2b2 100644 --- a/src/MalwarelyticsConfig.ts +++ b/src/MalwarelyticsConfig.ts @@ -525,6 +525,9 @@ export interface MalwarelyticsAndroidRaspRootDetectionConfig extends Malwarelyti exitOnRootMinConfidence?: number } +/** + * Configuration for Android debugger RASP event detection. + */ export interface MalwarelyticsAndroidRaspDebuggerDetectionConfig extends MalwarelyticsAndroidRaspDetectionConfig { debuggerTypes?: DebuggerType[]; diff --git a/src/internal/MalwarelyticsModule.ts b/src/internal/MalwarelyticsModule.ts index 9d6fcb9..adbf946 100644 --- a/src/internal/MalwarelyticsModule.ts +++ b/src/internal/MalwarelyticsModule.ts @@ -18,7 +18,7 @@ import { NativeModules, Platform } from 'react-native'; import type { NativeModule } from 'react-native'; import type { EventType } from './EventHelper'; import type { MalwarelyticsInitializationResult, MalwarelyticsState } from '../Malwarelytics'; -import type { MalwarelyticsConfig } from '../MalwarelyticsConfig'; +import type { MalwarelyticsAppleDetectableApp, MalwarelyticsConfig } from '../MalwarelyticsConfig'; import { MalwarelyticsError } from '../MalwarelyticsError'; import { RaspEventType } from './RaspEvent'; import { SmartProtectionResult } from '../model/antivirus/SmartProtectionResult'; @@ -41,6 +41,8 @@ export interface MalwarelyticsModuleIfc extends NativeModule { initialize(config: MalwarelyticsConfig): Promise shutdown(clearAvUserId: boolean): Promise getState(): Promise + // config + getKnownDetectableApps(): Promise // Internal getSupportedEvents(): Promise> // Device / USER ids From 09e3b83e845f6843c17dfa0a33e93065be8219c9 Mon Sep 17 00:00:00 2001 From: Tomas Kypta Date: Wed, 22 May 2024 23:42:36 +0200 Subject: [PATCH 3/6] Improve apple screen capture detection with hide action --- example/src/Config.ts | 17 +++++ ios/MalwarelyticsConfig.swift | 127 +++++++++++++++++++++++++++------- ios/RNHelper.swift | 7 ++ src/MalwarelyticsConfig.ts | 55 ++++++++++++++- 4 files changed, 179 insertions(+), 27 deletions(-) diff --git a/example/src/Config.ts b/example/src/Config.ts index 75c49c1..268ce7f 100644 --- a/example/src/Config.ts +++ b/example/src/Config.ts @@ -45,6 +45,23 @@ const defaultConfig: MalwarelyticsConfig = { appPresence: { action: "NOTIFY", apps: [] // filled in later + }, + screenCapture: { + action: "HIDE", + overlay: { + type: "COLOR", + color: { + red: 128, + blue: 0, + green: 0, + alpha: 255 + } + // Example of IMAGE overlay + // type: "IMAGE", + // image: { + // name: "" // The name of the file or image asset. + // } + } } } }, diff --git a/ios/MalwarelyticsConfig.swift b/ios/MalwarelyticsConfig.swift index 74c9023..c85d507 100644 --- a/ios/MalwarelyticsConfig.swift +++ b/ios/MalwarelyticsConfig.swift @@ -161,31 +161,6 @@ extension AppProtectionRaspConfig.DebuggerDetectionConfig { static let jsObj = "MalwarelyticsAppleDebuggerDetectionConfig" } -extension AppProtectionRaspConfig.ScreenCaptureDetectionConfig { - /// Create AppProtectionRaspConfig.ScreenCaptureDetectionConfig from JSON dictionary. - /// - Parameter dict: Dictionary with configuration. - /// - Returns: Configuration created from JSON dictionary. - static func fromDictionary(_ dict: ConfigDictionary?) throws -> AppProtectionRaspConfig.ScreenCaptureDetectionConfig? { - // JS is using limited, basic config for screen capture detection. - guard let basicConfig = try AppProtectionRaspConfig.DetectionConfig.fromDictionary(dict) else { - return nil - } - switch basicConfig { - case .noAction: - return .noAction - case .notify: - return .notify - case .exit(let exitUrl): - return .exit(exitUrl) - @unknown default: - throw ModuleError.invalidConfig(configPath: "action", object: jsObj) - } - } - - /// Name of configuration object repodted in the exception. - static let jsObj = "MalwarelyticsAppleBasicDetectionConfig" -} - extension AppProtectionRaspConfig.RepackageConfig { /// Create AppProtectionRaspConfig.RepackageConfig from JSON dictionary. /// - Parameter dict: Dictionary with configuration. @@ -220,6 +195,108 @@ extension AppProtectionRaspConfig.RepackageConfig { static let jsObj = "MalwarelyticsAppleRepackagingDetectionConfig" } +extension AppProtectionRaspConfig.ScreenCaptureDetectionConfig { + /// Create AppProtectionRaspConfig.ScreenCaptureDetectionConfig from JSON dictionary. + /// - Parameter dict: Dictionary with configuration. + /// - Returns: Configuration created from JSON dictionary. + static func fromDictionary(_ dict: ConfigDictionary?) throws -> AppProtectionRaspConfig.ScreenCaptureDetectionConfig? { + guard let dict = dict else { + return nil + } + let action = try dict.stringAt("action", in: jsObj) + let exitUrl = try dict.stringOptAt("exitUrl", in: jsObj) + let overlay: AppProtectionRaspConfig.ScreenCaptureDetectionConfig.Overlay? = try .fromDictionary(dict.dictOptAt("overlay", in: jsObj)) + switch action { + case "NO_ACTION": + return .noAction + case "NOTIFY": + return .notify + case "HIDE": + return .hide(overlay ?? AppProtectionRaspConfig.ScreenCaptureDetectionConfig.Overlay.default) + case "EXIT": + return .exit(exitUrl) + default: + throw ModuleError.invalidConfig(configPath: "action", object: jsObj) + } + } + + /// Name of configuration object repodted in the exception. + static let jsObj = "MalwarelyticsAppleScreenCaptureDetectionConfig" +} + +extension AppProtectionRaspConfig.ScreenCaptureDetectionConfig.Overlay { + /// Create AppProtectionRaspConfig.ScreenCaptureDetectionConfig.Overlay from JSON dictionary. + /// - Parameter dict: Dictionary with configuration. + /// - Returns: Configuration created from JSON dictionary. + static func fromDictionary(_ dict: ConfigDictionary?) throws -> AppProtectionRaspConfig.ScreenCaptureDetectionConfig.Overlay? { + guard let dict = dict else { + return nil + } + let type = try dict.stringAt("type", in: jsObj) + let color: UIColor? = try .fromDictionary(dict.dictOptAt("color", in: jsObj)) + let image: UIImage? = try .fromDictionary(dict.dictOptAt("image", in: jsObj)) + switch type { + case "DEFAULT": + return .`default` + case "COLOR": + guard let color = color else { + throw ModuleError.invalidConfig(configPath: "color", object: jsObj) + } + return .color(color) + case "IMAGE": + guard let image = image else { + throw ModuleError.invalidConfig(configPath: "image", object: jsObj) + } + return .image(image) + default: + throw ModuleError.invalidConfig(configPath: "type", object: jsObj) + } + } + + /// Name of configuration object repodted in the exception. + static let jsObj = "MalwarelyticsAppleOverlay" +} + +extension UIColor { + /// Create UIColor from JSON dictionary. + /// - Parameter dict: Dictionary with configuration. + /// - Returns: Configuration created from JSON dictionary. + static func fromDictionary(_ dict: ConfigDictionary?) throws -> UIColor? { + guard let dict = dict else { + return nil + } + let red = try dict.intAt("red", in: jsObj) + let green = try dict.intAt("green", in: jsObj) + let blue = try dict.intAt("blue", in: jsObj) + let alpha = try dict.intAt("alpha", in: jsObj) + return UIColor( + red: CGFloat(Double(red)/255.0), + green: CGFloat(Double(green)/255.0), + blue: CGFloat(Double(blue)/255.0), + alpha: CGFloat(Double(alpha)/255.0) + ) + } + + /// Name of configuration object repodted in the exception. + static let jsObj = "MalwarelyticsAppleColor" +} + +extension UIImage { + /// Create UIImage from JSON dictionary. + /// - Parameter dict: Dictionary with configuration. + /// - Returns: Configuration created from JSON dictionary. + static func fromDictionary(_ dict: ConfigDictionary?) throws -> UIImage? { + guard let dict = dict else { + return nil + } + let name = try dict.stringAt("name", in: jsObj) + return UIImage(imageLiteralResourceName: name) + } + + /// Name of configuration object repodted in the exception. + static let jsObj = "MalwarelyticsAppleImage" +} + extension AppProtectionRaspConfig.SimpleDetectionConfig { /// Create AppProtectionRaspConfig.SimpleDetectionConfig from JSON dictionary. /// - Parameter dict: Dictionary with configuration. diff --git a/ios/RNHelper.swift b/ios/RNHelper.swift index 30aa140..a7a2d4c 100644 --- a/ios/RNHelper.swift +++ b/ios/RNHelper.swift @@ -88,6 +88,13 @@ extension ConfigDictionary { return v } + func intAt(_ path: String, in object: String? = nil) throws -> Int { + guard let v = try objectAt(path: path, object: object, required: true) as? Int else { + throw ModuleError.missingConfig(configPath: path, object: object) + } + return v + } + func dictAt(_ path: String, in object: String? = nil, required: Bool = false, fallback: ConfigDictionary = [:]) throws -> ConfigDictionary { guard let v = try objectAt(path: path, object: object, required: required) as? ConfigDictionary else { if required { diff --git a/src/MalwarelyticsConfig.ts b/src/MalwarelyticsConfig.ts index a48e2b2..06d3cf8 100644 --- a/src/MalwarelyticsConfig.ts +++ b/src/MalwarelyticsConfig.ts @@ -150,7 +150,7 @@ export interface MalwarelyticsAppleRaspConfig { /** * Screen capturing detection. The default action is `NOTIFY`. */ - screenCapture?: MalwarelyticsAppleBasicDetectionConfig; + screenCapture?: MalwarelyticsAppleScreenCaptureDetectionConfig; /** * VPN detection config. The default value is `NOTIFY`. */ @@ -267,6 +267,18 @@ export interface MalwarelyticsAppleDebuggerDetectionConfig { exitUrl?: string; } +/** + * Configuration of the screen capture detection behavior. + */ +export interface MalwarelyticsAppleScreenCaptureDetectionConfig { + /** Behavior of the detection */ + action: MalwarelyticsAppleScreenCaptureDetectionAction; + /** This URL will be open in the default browser when app is terminated in case that the `action` is `EXIT`. */ + exitUrl?: string; + /** Overlay that will be displayed when screen capture is detected in case that the `action` is `HIDE`. */ + overlay?: MalwarelyticsAppleOverlay; +} + /** * Configuration of the simple detection behavior. */ @@ -301,6 +313,32 @@ export interface MalwarelyticsAppleDetectableApp { tag?: string; } +/** + * Configuration of the screen capture overlay. + */ +export interface MalwarelyticsAppleOverlay { + type: MalwarelyticsAppleOverlayType; + color?: MalwarelyticsAppleColor; + image?: MalwarelyticsAppleImage; +} + +/** + * UIColor abstraction for configuration. + */ +export interface MalwarelyticsAppleColor { + red: number; + green: number; + blue: number; + alpha: number; +} + +/** + * UIImage abstraction for configuration. + */ +export interface MalwarelyticsAppleImage { + name: string; +} + /** * Category of MalwarelyticsAppleDetectableApp * REMOTE_DESKTOP - Remote desktop apps are apps that can screen cast phone screen. @@ -322,6 +360,14 @@ export type MalwarelyticsAppleDebuggerDetectionAction = "NO_ACTION" | "NOTIFY" | */ export type MalwarelyticsAppleDetectionAction = "NO_ACTION" | "NOTIFY" | "EXIT"; +/** + * NO_ACTION - do nothing + * NOTIFY - notify via the observer + * HIDE - hide app's content with an overlay when screen capture is detected + * EXIT - exit the app + */ +export type MalwarelyticsAppleScreenCaptureDetectionAction = "NO_ACTION" | "NOTIFY" | "HIDE" | "EXIT"; + /** * NO_ACTION - do nothing * NOTIFY - notify via the observer @@ -334,7 +380,12 @@ export type MalwarelyticsAppleSimpleDetectionAction = "NO_ACTION" | "NOTIFY"; */ export type MalwarelyticsAppleAppPresenceDetectionAction = "MANUAL" | "NOTIFY" - +/** + * DEFAULT - default cover with a solid color and an application icon + * COLOR - cover with a solid color + * IMAGE - cover with an image + */ +export type MalwarelyticsAppleOverlayType = "DEFAULT" | "COLOR" | "IMAGE"; // --------------------------------------------------------------------------------------- // From 2f935c27656db367f9126200888152fb9840f898 Mon Sep 17 00:00:00 2001 From: Tomas Kypta Date: Wed, 22 May 2024 23:48:08 +0200 Subject: [PATCH 4/6] Update release notes --- docs/Release-Notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/Release-Notes.md b/docs/Release-Notes.md index ee05585..8736e39 100644 --- a/docs/Release-Notes.md +++ b/docs/Release-Notes.md @@ -4,6 +4,7 @@ ### Release 1.0.3-dev +- Update Malwarelytics for Apple to 2.1.1 (#53) - Update Malwarelytics for Android to 1.0.2 (#52) - Update project configuration (#56) - Fix iOS build (#57) From 3bf25e4789cf1bc22f331b3e95004c1d9f88d1d2 Mon Sep 17 00:00:00 2001 From: Tomas Kypta Date: Thu, 23 May 2024 12:35:59 +0200 Subject: [PATCH 5/6] Update docs --- docs/Configuration.md | 22 +++++++++++++++++++++- docs/Installation.md | 4 ++-- docs/Usage-RASP.md | 2 +- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/Configuration.md b/docs/Configuration.md index 3867ca8..298aacd 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -49,7 +49,18 @@ await Malwarelytics.sharedInstance.initialize({ // that prevents the debugger to be connected to the process. debugger: { action: 'BLOCK' }, // Action defined when device's screen is being captured. - screenCapture: { action: 'NOTIFY' }, + screenCapture: { + action: 'HIDE', // Hide the app screen + overlay: { + type: 'COLOR', // Use overlay with the specified color + color: { + red: 128, + blue: 0, + green: 0, + alpha: 255 + } + } + }, // Action defined when reverse engineering tools are detected on the device. reverseEngineeringTools: { action: 'EXIT' }, // Action defined when HTTP proxy is configured on the device. @@ -63,6 +74,15 @@ await Malwarelytics.sharedInstance.initialize({ exitUrl: "https://wultra.com/?exit=repackage", // Array with Base64 encoded certificates that used to sign app package. trustedCertificates: ["BASE64"] + }, + // Action defined when there's an ongoing call + call: { + action: "NOTIFY" + }, + // Action defined when a presence of an app from the list is detected. + appPresence: { + action: "NOTIFY", + apps: iosDetectableApps // Array with apps that can be detected. Predefined array can be obtained from the native SDK with await Malwarelytics.sharedInstance.getKnownDetectableApps() } }, // Optional configuration of the events collection. diff --git a/docs/Installation.md b/docs/Installation.md index 6d1d7bd..f1c37a3 100644 --- a/docs/Installation.md +++ b/docs/Installation.md @@ -7,7 +7,7 @@ Malwarelytics for React Native is distributed as a NPM package. The private Mave ## Platform Support -Malwarelytics for React Native __supports iOS 11 and Android 5.1__ (SDK version 22) and above. The React Native 0.71 and above is recommended for your application. +Malwarelytics for React Native __supports iOS 13.4 and Android 6.0__ (SDK version 23) and above. The React Native 0.74 and above is recommended for your application. ## Configure Wultra's Artifactory @@ -35,7 +35,7 @@ allProjects { } ``` -In the same file, look for `minSdkVersion` and make sure that it's higher or equal to `22`. +In the same file, look for `minSdkVersion` and make sure that it's higher or equal to `23`. If you don't want to expose your credentials in the gradle file, then use the script to load the credentials from `local.properties`. For example: diff --git a/docs/Usage-RASP.md b/docs/Usage-RASP.md index eee9cd6..a62696e 100644 --- a/docs/Usage-RASP.md +++ b/docs/Usage-RASP.md @@ -118,6 +118,7 @@ console.log(`Screen sharing info: ${JSON.stringify(screenSharing)}`); console.log(`Is VPN active = ${await rasp.isVpnActive()}`); console.log(`Phone call = ${await rasp.isOnCall()}`); +console.log(`App presence info = ${JSON.stringify(await rasp.getAppPresenceInfo())}`); // Apple specific @@ -138,7 +139,6 @@ if (Platform.OS == 'android') { console.log(`Bad Tapjacking capable app is present = ${await rasp.isBadTapjackingCapableAppPresent()}`); console.log(`Developer options enabled = ${await rasp.isDeveloperOptionsEnabled()}`); console.log(`Active call info = ${JSON.stringify(await rasp.getActiveCallInfo())}`); - console.log(`App presence info = ${JSON.stringify(await rasp.getAppPresenceInfo())}`); } ``` From 3c4623cbca731b3a0accddefc71d0f20ae4d1bcd Mon Sep 17 00:00:00 2001 From: Tomas Kypta Date: Thu, 23 May 2024 13:27:00 +0200 Subject: [PATCH 6/6] Raise example's max heap space --- example/android/gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/android/gradle.properties b/example/android/gradle.properties index c63a3c5..f50e60e 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -10,7 +10,7 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m -org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m +org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit