Threat-Events™, In-App Threat Intelligence in Flutter Apps with Obj-C & Java

Last updated September 22, 2024 by Appdome

This guide walks you through integrating Appdome’s Threat-Events™ into your Flutter applications, offering a step-by-step approach to enhance mobile app security.

What are Threat-Events?

Appdome Threat-Events is a robust threat-intelligence framework for Android and iOS apps. It consists of three elements: a Threat Event, the data from each Threat Event, and the Threat-Score™.

Threat-Events streamlines the process for mobile developers by enabling them to register for, listen to, and consume real-time attack and threat data, courtesy of Appdome’s suite of mobile app security, anti-fraud, and mobile anti-bot protection measures. This functionality offers mobile app developers the ability to customize business logic and user experience based on the user’s risk profile and the specific attack or threat presented but also ensures that their mobile application workflows are aware of attacks and threats. Furthermore, it facilitates the passing of threat data to other systems of record, such as app servers, mobile fraud analysis systems, SIEMs, and other data collection points.

The purpose of Threat-Events is to enable Android and iOS applications to adapt and respond to mobile app attacks and threats in real time. This will ensure the safety of user data and transactions.

Mobile Application Threat-Events vs. Threat-Scores

Appdome Threat-Events can be used as a stand-alone implementation in Flutter Apps or in combination with Threat-Scores. Threat-Events provide in-app notifications of each attack or threat, as well as the metadata associated with the attack. Threat Scores provide the mobile developer with a Threat-Event event score and the combined (aggregated) mobile end-user risk score at the time of the notification.

Prerequisites

Before implementing Threat-Events or Threat-Scores in your Flutter App, ensure that the following conditions are met:

  • Appdome account (create a free Appdome account here)
  • Mobile App (.ipa for iOS, or .apk or .aab for Android)
  • Appdome account class – Appdome DEV or higher.
  • Mobile Application Source Code.
  • Signing Credentials (e.g., signing certificates and provisioning profile) – see Signing Secure Android apps and Signing Secure iOS apps.
  • Threat-Events and/or Threat-Scores have been enabled ( turned ON) for the specific protection
  • You are using the correct identifiers for the Threat-Events for each protection.

Note: You can find the specific identifiers for each Threat-Event and Threat-Score in the knowledge base article associated with each protection.

The figure below shows where you can find Threat-Events and Threat-Scores for each of the runtime mobile app security, anti-fraud, anti-malware, mobile antibot, and other protections available on Appdome.

Android

Root Detection Threat Events

iOS
Jailbreak Detection

To enable Threat-Events with any runtime protection, select the check box next to Threat-Events for that feature. Doing so will enable (turn ON) Threat-Events for that feature. To enable Threat-Scores for any runtime protection, click the up/down arrow associated with Threat-Scores to assign a specific score to each protection.
Threat-Scores must have a value greater than zero (0) and less than a thousand (1,000).

Threat-Events and Threat-Scores can be used with or in place of server-based mobile anti-fraud solutions.

Communicating with Native Platforms in Flutter

Flutter provides various approaches to bridge the gap between its Dart code and platform-specific functionality on Android and iOS. Some common options include:

  • Event Channels: This is the go-to method for most situations. It sets up named channels enabling bidirectional communication between Dart and native code.
  • Platform Views: This technique allows you to integrate native UI components, granting access to features like maps or cameras directly within your Flutter app.
  • Method Channels: A specialized type of platform channel, it specifically focuses on invoking methods on the native side and retrieving results back in Dart.

Choose the approach that best suits your needs when bridging the gap between Flutter and native platforms Threat-Events.

Prioritizing Security with Event Channels:

Event channels are the optimal approach for integrating threat-events into your Flutter app due to their inherent security-centric advantages:

  • Leveraging Native Security: Appdome platform functionalities are specifically designed to detect and mitigate security threats on iOS and Android applications. By establishing an event channel, your Flutter app enables Appdome to seamlessly tap into these platform specific capabilities, gaining access to sophisticated threat detection mechanisms without compromising security by directly accessing native system resources.
  • Immediate Threat Awareness & Response: Event channels enable real-time communication between Appdome and your Flutter app, allowing for immediate report of detected threats.

Integrating Secure Event Channels into Your Flutter Application:

  1. Adjust EventChannel Name:
    • Locate ״_eventChannelName״ and replace ״yourEventChannel״ with a unique and descriptive EventChannel name relevant to your application.
  2. Customize Event Handling:
    • In the ״_onEvent״ method, customize the handling of received events based on your specific needs.
      import 'dart:async';
      import 'package:flutter/material.dart';
      import 'package:flutter/services.dart';
      class PlatformChannel extends StatefulWidget {
        const PlatformChannel({super.key});
        @override
        State<PlatformChannel> createState() => _PlatformChannelState();
      }
      class _PlatformChannelState extends State<PlatformChannel> {
        static const String _eventChannelName = 'yourEventChannel'; // Replace with your EventChannel name
        static const EventChannel _eventChannel = EventChannel(_eventChannelName);
        String _currentThreatEvent = 'No ThreatEvent detected';
        String? keyboardID;
        String? keyboardBlocked;
        String? defaultMessage;
        String? timeStamp;
        String? deviceID;
        String? deviceModel;
        String? osVersion;
        String? kernelInfo;
        String? deviceManufacturer;
        String? fusedAppToken;
        String? carrierPlmn;
        String? deviceBrand;
        String? deviceBoard;
        String? buildHost;
        String? buildUser;
        String? sdkVersion;
        @override
        void initState() {
          super.initState();
          _eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
        }
        void _onEvent(Object? event) {
          setState(() {
            // Adapt this section based on your specific event data structure
            var eventData = event as Map;
            // Example: Accessing 'externalID' field from the event
            var externalID = eventData['externalID'];
            // Customize the rest of the fields based on your event structure
            keyboardID = eventData['keyboard'];
            keyboardBlocked = eventData['blocked'];
            defaultMessage = eventData['defaultMessage'];
            timeStamp = eventData['timestamp'];
            deviceID = eventData['deviceID'];
            deviceModel = eventData['deviceModel'];
            osVersion = eventData['osVersion'];
            kernelInfo = eventData['kernelInfo'];
            deviceManufacturer = eventData['deviceManufacturer'];
            fusedAppToken = eventData['fusedAppToken'];
            carrierPlmn = eventData['carrierPlmn'];
            deviceBrand = eventData['deviceBrand'];
            deviceBoard = eventData['deviceBoard'];
            buildHost = eventData['buildHost'];
            buildUser = eventData['buildUser'];
            sdkVersion = eventData['sdkVersion'];
            _currentThreatEvent = "Detected ThreatEvent: $externalID.\n" +
                                  "Keyboard ID: $keyboardID\n" +
                                  "Keyboard Blocked: $keyboardBlocked\n" +
                                  "Default Message: $defaultMessage\n" +
                                  "Timestamp: $timeStamp\n" +
                                  "Device ID: $deviceID\n" +
                                  "Device Model: $deviceModel\n" +
                                  "OS Version: $osVersion\n" +
                                  "Kernel Info: $kernelInfo\n" +
                                  "Device Manufacturer: $deviceManufacturer\n" +
                                  "Fused App Token: $fusedAppToken\n" +
                                  "Carrier PLMN: $carrierPlmn\n" +
                                  "Device Brand: $deviceBrand\n" +
                                  "Device Board: $deviceBoard\n" +
                                  "Build Host: $buildHost\n" +
                                  "Build User: $buildUser\n" +
                                  "SDK Version: $sdkVersion\n";
          });
        }
        void _onError(Object error) {
          setState(() {
            _currentThreatEvent = 'Failed To Get ThreatEvent: ${error}.';
          });
        }
        @override
        Widget build(BuildContext context) {
          return Material(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: <Widget>[
                Text(_currentThreatEvent),
              ],
            ),
          );
        }
      }
      void main() {
        runApp(const MaterialApp(home: PlatformChannel()));
      }
      

Platform-Specific iOS Implementation

  1. Open your iOS folder in the Flutter project.
  2. Select AppDelegate.h
  3. Insert the <FlutterStreamHandler>

    #import <UIKit/UIKit.h>
    #import <Flutter/Flutter.h>
    @interface AppDelegate : FlutterAppDelegate<FlutterStreamHandler>
    @end
    
  4. Select AppDelegate.m
  5. Locate @”yourEventChannel” and replace it with a unique and descriptive EventChannel name relevant to your application.
    #import "AppDelegate.h"
    #import <Flutter/Flutter.h>
    #import "GeneratedPluginRegistrant.h"
    #import "ThreatEventReceiver.h"
    @implementation AppDelegate {
      FlutterEventSink _eventSink;
      ThreatEventReceiver *_threatEventReceiver;
    }
    - (BOOL)application:(UIApplication*)application
        didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
      [GeneratedPluginRegistrant registerWithRegistry:self];
      FlutterViewController* controller =
          (FlutterViewController*)self.window.rootViewController;
      FlutterEventChannel* threatEventBridge = [FlutterEventChannel
                                              eventChannelWithName:@"yourEventChannel"
                                              binaryMessenger:controller];
      [threatEventBridge setStreamHandler:self];
      return [super application:application didFinishLaunchingWithOptions:launchOptions];
    }
    - (FlutterError*)onListenWithArguments:(id)arguments
                                 eventSink:(FlutterEventSink)eventSink {
      _threatEventReceiver = [[ThreatEventReceiver alloc] init];
      _threatEventReceiver.eventSink = eventSink;
      return nil;
    }
    - (FlutterError*)onCancelWithArguments:(id)arguments {
      return nil;
    }
    @end
    
  6. Select ThreatEventReceiver.h file.

    #import <Foundation/Foundation.h>
    #import <Flutter/Flutter.h>
    @interface ThreatEventReceiver : NSObject
    @property (nonatomic, copy) FlutterEventSink eventSink;
    - (instancetype)init;
    @end
  7. Open the ThreatEventReceiver.m  file.
  8. Modify the event list to fit with the  event names you selected during fuse to be passed as ThreatEvent.
    #import "ThreatEventReceiver.h"
    @implementation ThreatEventReceiver
    - (instancetype)init {
        self = [super init];
        if (self) {
            // List of constant names
            NSArray<NSString *> *notificationNames = @[ // Replace with your event names
                @"BlockedKeyboardEvent",
                @"BlockedClipboardEvent",
                @"JailbrokenDevice",
                @"SslCertificateValidationFailed",
                @"SslNonSslConnection",
                @"SslServerCertificatePinningFailed",
                @"UrlWhitelistFailed",
                @"BlockedScreenCaptureEvent",
                @"SslIncompatibleCipher",
                @"SslIncompatibleVersion",
                @"SslInvalidCertificateChain",
                @"SslInvalidMinRSASignature",
                @"SslInvalidMinECCSignature",
                @"SslInvalidMinDigest",
                @"AppIntegrityError"
            ];
            // Add observers for each constant name
            for (NSString *notificationName in notificationNames) {
                [[NSNotificationCenter defaultCenter] addObserver:self
                                                         selector:@selector(handleNotification:)
                                                             name:notificationName
                                                           object:nil];
            }
        }
        return self;
    }
    - (void)dealloc {
        // Remove all observers when the object is deallocated
        [[NSNotificationCenter defaultCenter] removeObserver:self];
        if (self.eventSink) {
            self.eventSink = nil;
        }
    }
    - (void)handleNotification:(NSNotification *)notification {
        // Handle the notification here
        NSLog(@"Received notification: %@", notification.name);
        if (!self.eventSink) return;
        if (notification.name) {
            self.eventSink([notification userInfo]);
        } else {
            self.eventSink([FlutterError errorWithCode:@"UNAVAILABLE"
                                            message:@"ThreatEvent action is null"
                                            details:nil]);
        }
    }
    @end

Platform-Specific Android Implementation – Java

  1. Open your Android folder in the Flutter project.
  2. Select the MainActivity.java file
  3. Locate the YOUR_EVENT_CHANNEL field and replace it with a unique and descriptive EventChannel name relevant to your application.
    import androidx.annotation.NonNull;
    import io.flutter.embedding.android.FlutterActivity;
    import io.flutter.embedding.engine.FlutterEngine;
    import io.flutter.plugin.common.EventChannel;
    import io.flutter.plugin.common.EventChannel.EventSink;
    import io.flutter.plugin.common.EventChannel.StreamHandler;
    public class MainActivity extends FlutterActivity {
      private static final String YOUR_EVENT_CHANNEL = "yourEventChannel"; // Replace with your EventChannel name
      @Override
      public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
        new EventChannel(flutterEngine.getDartExecutor(), YOUR_EVENT_CHANNEL).setStreamHandler(
          new StreamHandler() {
            private ThreatEventReceiver threatEventReceiver;
            @Override
            public void onListen(Object arguments, EventSink events) {
              threatEventReceiver = new ThreatEventReceiver();
              threatEventReceiver.init(MainActivity.this, events);
            }
            @Override
            public void onCancel(Object arguments) {
              threatEventReceiver.stop();
              threatEventReceiver = null;
            }
          }
        );
      }
    }
  4. Open the ThreatEventReceiver.java file
  5. Modify the event list to fit with the event names you selected during fuse to be passed as ThreatEvent.
    Note: Create a ThreatEventReceiver.java file in your application project if you do not have an existing one.

    import android.content.BroadcastReceiver;
    import android.content.Context;
    import android.content.Intent;
    import android.content.IntentFilter;
    import android.os.Build;
    import android.os.Bundle;
    import java.util.Map;
    import java.util.HashMap;
    import io.flutter.plugin.common.EventChannel.EventSink;
    public class ThreatEventReceiver {
        private BroadcastReceiver receiver;
        private Context context;
        private static final String TAG = "ThreatEventReceiver";
        private final static String BLOCKED_KEYBOARD = "BlockedKeyboardEvent"; // Replace with your event names
        private final static String BLOCKED_CLIPBOARD = "BlockedClipboardEvent";
        private final static String ROOTED_DEVICE = "RootedDevice";
        private final static String UNKNOWN_SOURCES = "UnknownSourcesEnabled";
        private final static String DEVELOPER_OPTIONS = "DeveloperOptionsEnabled";
        private final static String SSL_VALIDATION_FAILED = "SslCertificateValidationFailed";
        private final static String SSL_NON_SSL_CONNECTION = "SslNonSslConnection";
        private final static String SSL_CERT_PINNING_FAILED = "SslCertificatePinningFailed";
        private final static String ACCESS_OUTSIDE_WHITELIST = "UrlWhitelistFailed";
        private final static String SSL_INCOMPATIBLE_CIPHER = "SslIncompatibleCipher";
        private final static String SSL_INCOMPATIBLE_TLS = "SslIncompatibleVersion";
        private final static String SSL_INVALID_CA_CHAIN = "SslInvalidCertificateChain";
        private final static String SSL_INVALID_RSA_SIGNATURE = "SslInvalidMinRSASignature";
        private final static String SSL_INVALID_ECC_SIGNATURE = "SslInvalidMinECCSignature";
        private final static String SSL_INVALID_DIGEST = "SslInvalidMinDigest";
        private final static String BLOCKED_MANUFACTURER = "BannedManufacturer";
        // Only available when ONEShield Threat Events are enabled
        private final static String TAMPERED_APP = "AppIntegrityError";
        private final static String EMULATOR_FOUND = "EmulatorFound";
        // Explicitly state that the ThreatEvents BroadcastReceiver should not be exported
        private void registerReceiverWithFlags(IntentFilter intentFilter) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                context.registerReceiver(receiver, intentFilter, Context.RECEIVER_NOT_EXPORTED);
            } else {
                context.registerReceiver(receiver, intentFilter);
            }
        }
        public void init(Context context, final EventSink events) {
            this.context = context;
            receiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    if (intent.getAction() != null) {
                        Map<String, Object> map = new HashMap<>();
                        Bundle extras = intent.getExtras();
                        if (extras != null) {
                            for (String key : extras.keySet()) {
                                Object value = extras.get(key);
                                map.put(key, value);
                            }
                            events.success(map);
                        }
                    } else {
                        events.error("UNAVAILABLE", "ThreatEvent action is null", null);
                    }
                }
            };
            registerReceiverWithFlags(new IntentFilter(BLOCKED_KEYBOARD));
            registerReceiverWithFlags(new IntentFilter(BLOCKED_CLIPBOARD));
            registerReceiverWithFlags(new IntentFilter(ROOTED_DEVICE));
            registerReceiverWithFlags(new IntentFilter(UNKNOWN_SOURCES));
            registerReceiverWithFlags(new IntentFilter(DEVELOPER_OPTIONS));
            registerReceiverWithFlags(new IntentFilter(SSL_VALIDATION_FAILED));
            registerReceiverWithFlags(new IntentFilter(SSL_NON_SSL_CONNECTION));
            registerReceiverWithFlags(new IntentFilter(SSL_CERT_PINNING_FAILED));
            registerReceiverWithFlags(new IntentFilter(ACCESS_OUTSIDE_WHITELIST));
            registerReceiverWithFlags(new IntentFilter(SSL_INCOMPATIBLE_CIPHER));
            registerReceiverWithFlags(new IntentFilter(SSL_INCOMPATIBLE_TLS));
            registerReceiverWithFlags(new IntentFilter(SSL_INVALID_CA_CHAIN));
            registerReceiverWithFlags(new IntentFilter(SSL_INVALID_RSA_SIGNATURE));
            registerReceiverWithFlags(new IntentFilter(SSL_INVALID_ECC_SIGNATURE));
            registerReceiverWithFlags(new IntentFilter(SSL_INVALID_DIGEST));
            registerReceiverWithFlags(new IntentFilter(BLOCKED_MANUFACTURER));
            // Only available when ONEShield Threat Events are enabled
            registerReceiverWithFlags(new IntentFilter(TAMPERED_APP));
            registerReceiverWithFlags(new IntentFilter(EMULATOR_FOUND));
        }
        // This method should be called during the Activity.onPause() if the context used when calling the init() method is an Activity context
        public void stop() {
            context.unregisterReceiver(receiver);
        }
    }

Compatibility with Android 14

Compatibility with Android 14 is seamless through Flutter EventChannel. Utilizing native platform implementations, the code sample provided above ensures that no modifications are needed to make your app fully compatible with Android 14.

Special Considerations

      • Replace the event identifiers in your ThreatEventReceiver according to the selections you made while fusing the application. To find the relevant Threat-Event context key, search for the specific feature via our knowledge base and navigate to the code sample section in the relevant article.
      • You can stack multiple listeners for various events for comprehensive threat detection and response capabilities.
      • Refer to the official documentation on EventChannel, BroadcastReceiver, and NSNotificationCenter for detailed information and advanced usage. And Alternative methods of implementation.

    Meta-Data for Mobile Application Threat-Events and Threat-Scores

    Below is the list of metadata that can be associated with each mobile application Threat-Event and Threat-Score in Flutter Apps.

    Threat-Event Context Keys
    message Message displayed for the user on event
    externalID The external ID of the event which can be listened via Threat Events
    osVersion OS version of the current device
    deviceModel Current device model
    deviceManufacturer The manufacturer of the current device
    fusedAppToken The task ID of the Appdome fusion of the currently running app
    kernelInfo Info about the kernel: system name, node name, release, version and machine.
    carrierPlmn PLMN of the device
    deviceID Current device ID
    reasonCode Reason code of the occurred event
    buildDate Appdome fusion date of the current application
    devicePlatform OS name of the current device
    carrierName Carrier name of the current device
    updatedOSVersion Is the OS version up to date
    deviceBrand Brand of the device
    deviceBoard Board of the device
    buildUser Build user
    buildHost Build host
    sdkVersion Sdk version
    timeZone Time zone
    deviceFaceDown Is the device face down
    locationLong Location long
    locationLat Location lat
    locationState Location state
    wifiSsid Wifi SSID
    wifiSsidPermissionStatus Wifi SSID permission status

    Some or all of the meta-data for each mobile application Threat-Event and Threat-Score can be consumed in Flutter Apps at the discretion of the mobile developer and used, in combination with other mobile application data, to adapt the business logic or user experience when one or more attacks or threats are present.

    Using Conditional Enforcement for Mobile Application Threat-Events and Threat-Scores

    Conditional Enforcement is an extension to Appdome’s mobile application Threat-Event framework. By using conditional enforcement, developers can control when Appdome enforcement of each mobile application protection takes place or invoke backup, failsafe, and enforcement to any in-app enforcement used by the mobile developer.
    For more information on using conditional enforcement with your Threat-Event implementation, please contact support.appdome.com

    Verifying Threat-Events in Flutter Apps

    After you have implemented the required Threat-Event code in your Flutter Apps, you can confirm that the Appdome protections in the Flutter Apps properly recognize your Threat-Event implementation by reviewing the Certified Secure™ DevSecOps certificate for your build on Appdome.

    Ios Android Cert 2 1000px

    In the Certified Secure DevSecOps certificate, an incorrect implementation of Threat-Events in your mobile application is displayed as shown below.

    Threat Events Wrong Implementation Ios Android 2

    For information on how to retrieve the Certified Secure DevSecOps certification for your mobile application on Appdome, please visit the knowledge base article: Using Certified Secure™ Android & iOS Apps Build Certification in DevOps CI/CD

    Related Articles

    Thank you!

    Thanks for visiting Appdome! Our mission is to secure every app on the planet by making mobile app security easy. We hope we’re living up to the mission with your project.

Appdome

Want a Demo?

Threat-Events™ UX/UI Control

AlanWe're here to help
We'll get back to you in 24 hours to schedule your demo.