This codelab demonstrates how to implement Mobile Threat Detection (MTD) flow using the react-native-rdna-client npm plugin. MTD now performs a synchronous check during the RELID SDK initialization to ensure critical threats are detected early. Once the SDK is successfully initialized, MTD continues monitoring asynchronously in the background to detect and respond to any emerging threats during runtime.
The code to get started is stored in a GitHub repository.
You can clone the repository using the following command:
git clone https://github.com/uniken-public/codelab-react-native.git
Navigate to the relid-MTD
folder in the repository you cloned earlier
react-native-rdna-client
plugin installed and configuredThe sample app provides a complete MTD implementation. Let's examine the key components:
Component | Purpose | Sample App Reference |
MTD Context | Global threat state management |
|
Threat Modal | UI for displaying threats |
|
Event Handling | Extended event manager |
|
Threat Types | TypeScript interfaces |
|
The plugin requires specific permissions for optimal MTD functionality:
iOS Configuration: Refer to the iOS Permissions Documentation for complete Info.plist configuration including:
Android Configuration: Refer to the Android Permissions Documentation for runtime and normal permissions required for MTD features.
The RELID SDK triggers two main MTD events during initialization:
Event Type | Description | User Action Required |
Non-terminating threats | User can choose to proceed or exit using takeActionOnThreats API | |
Critical threats | Application must exit immediately |
Define TypeScript interfaces for MTD threat handling:
// src/uniken/types/rdnaEvents.ts
export interface RDNAThreatInfo {
threatName: string;
threatMsg: string;
threatId: number;
threatCategory: string; // SYSTEM, APP, NETWORK
threatSeverity: string; // LOW, MEDIUM, HIGH
threatReason: string[] | string;
networkInfo: {
bssid: string;
maliciousAddress: string;
maliciousMacAddress: string;
ssid: string;
};
appInfo: {
appName: string;
appSha256: string;
packageName: string;
};
rememberActionForSession: number; // 0/1 for API, handled as boolean in implementation
configuredAction: string;
shouldProceedWithThreats: number; // 0/1 for API, handled as boolean in implementation
}
export interface RDNAUserConsentThreatsData {
threats: RDNAThreatInfo[];
}
export interface RDNATerminateWithThreatsData {
threats: RDNAThreatInfo[];
}
Understanding threat classification helps in implementing:
Category | Examples | Platform |
SYSTEM | Usb Debugging, Rooted Device | Android and iOS |
NETWORK | Network MITM, Unsecured Access Point | Android and iOS |
APP | Malware App, Repacked App | Only Android |
Extend your existing event manager to handle MTD events:
// src/uniken/services/rdnaEventManager.ts (additions)
class RdnaEventManager {
// Add MTD callback properties
private userConsentThreatsHandler?: RDNAUserConsentThreatsCallback;
private terminateWithThreatsHandler?: RDNATerminateWithThreatsCallback;
private registerEventListeners() {
// ... existing listeners ...
// Add MTD event listeners
this.listeners.push(
this.rdnaEmitter.addListener('onUserConsentThreats', this.onUserConsentThreats.bind(this)),
this.rdnaEmitter.addListener('onTerminateWithThreats', this.onTerminateWithThreats.bind(this)),
);
}
/**
* Handles security threat events requiring user consent
*/
private onUserConsentThreats(response: RDNAJsonResponse) {
try {
const threatArray = JSON.parse(response.response);
const userConsentData: RDNAUserConsentThreatsData = { threats: threatArray };
if (this.userConsentThreatsHandler) {
this.userConsentThreatsHandler(userConsentData);
}
} catch (error) {
console.error("Failed to parse user consent threats:", error);
}
}
/**
* Handles critical security threat events requiring app termination
*/
private onTerminateWithThreats(response: RDNAJsonResponse) {
try {
const threatArray = JSON.parse(response.response);
const terminateData: RDNATerminateWithThreatsData = { threats: threatArray };
if (this.terminateWithThreatsHandler) {
this.terminateWithThreatsHandler(terminateData);
}
} catch (error) {
console.error("Failed to parse terminate threats:", error);
}
}
public setUserConsentThreatsHandler(callback?: RDNAUserConsentThreatsCallback): void {
this.userConsentThreatsHandler = callback;
}
public setTerminateWithThreatsHandler(callback?: RDNATerminateWithThreatsCallback): void {
this.terminateWithThreatsHandler = callback;
}
}
Key features of MTD event handling:
The takeActionOnThreats API is only required for handling threats received through the onUserConsentThreats event. This allows the application to take appropriate action based on user consent.
The onTerminateWithThreats event is triggered only when critical threats are detected. In such cases, the SDK automatically terminates internally, and no further actions can be performed through the plugin until the SDK is reinitialized.
Add threat response capability to your RELID service:
// src/uniken/services/rdnaService.ts (addition)
export class RdnaService {
/**
* Takes action on detected security threats
* @param modifiedThreatsJson JSON string containing threat action decisions
* @returns Promise<RDNASyncResponse> that resolves with sync response structure
*/
async takeActionOnThreats(modifiedThreatsJson: string): Promise<RDNASyncResponse> {
return new Promise((resolve, reject) => {
RdnaClient.takeActionOnThreats(modifiedThreatsJson, response => {
console.log('RdnaService - Take action on threats response received');
const result: RDNASyncResponse = response;
if (result.error && result.error.longErrorCode === 0) {
console.log('RdnaService - Successfully took action on threats');
resolve(result);
} else {
const errorMessage =
result.error?.errorString ||
'Unknown error from takeActionOnThreats';
console.error(
'RdnaService - Take action on threats failed:',
errorMessage,
);
reject(result);
}
});
});
}
}
When responding to threats, two key parameters control the behavior:
Parameter | Purpose | Implementation Values | API Values |
| Whether to continue despite threats |
| Converted to boolean in JSON |
| Cache decision for session |
| Converted to boolean in JSON |
Note: While the API documentation may show these as number types (0/1), the actual implementation uses boolean values (true
/false
) for better code readability.
Create a React Context to manage MTD state across your application:
// src/uniken/MTDContext/MTDThreatContext.tsx
interface MTDThreatState {
isModalVisible: boolean;
threats: RDNAThreatInfo[];
isConsentMode: boolean;
isProcessing: boolean;
showThreatModal: (threats: RDNAThreatInfo[], isConsent: boolean) => void;
hideThreatModal: () => void;
handleProceed: () => void;
handleExit: () => void;
}
export const MTDThreatProvider: React.FC<MTDThreatProviderProps> = ({ children }) => {
const [isModalVisible, setIsModalVisible] = useState(false);
const [threats, setThreats] = useState<RDNAThreatInfo[]>([]);
const [isConsentMode, setIsConsentMode] = useState(false);
const [isProcessing, setIsProcessing] = useState(false);
useEffect(() => {
const eventManager = rdnaService.getEventManager();
eventManager.setUserConsentThreatsHandler((data: RDNAUserConsentThreatsData) => {
showThreatModal(data.threats, true);
});
eventManager.setTerminateWithThreatsHandler((data: RDNATerminateWithThreatsData) => {
showThreatModal(data.threats, false);
});
return () => {
console.log('MTDThreatProvider cleanup');
};
}, []);
};
The context handles user decisions by modifying threat objects:
const handleProceed = () => {
setIsProcessing(true);
// Modify all threats to proceed with action
const modifiedThreats = threats.map(threat => ({
...threat,
shouldProceedWithThreats: true,
rememberActionForSession: true,
threatReason: Array.isArray(threat.threatReason)
? threat.threatReason.join(',')
: threat.threatReason
}));
const threatsJsonString = JSON.stringify(modifiedThreats);
rdnaService.takeActionOnThreats(threatsJsonString)
.then(() => {
hideThreatModal();
})
.catch((error) => {
setIsProcessing(false);
Alert.alert('Failed to proceed with threats', error.errorString);
});
};
const handleExit = () => {
if (isConsentMode) {
setIsProcessing(true);
const modifiedThreats = threats.map(threat => ({
...threat,
shouldProceedWithThreats: false,
rememberActionForSession: true,
threatReason: Array.isArray(threat.threatReason)
? threat.threatReason.join(',')
: threat.threatReason
}));
const threatsJsonString = JSON.stringify(modifiedThreats);
rdnaService.takeActionOnThreats(threatsJsonString)
.then(() => {
// Application will exit via terminateWithThreats callback
})
.catch((error) => {
setIsProcessing(false);
Alert.alert('Failed to process threat action', error.errorString);
});
} else {
// Direct exit for terminate threats
BackHandler.exitApp();
}
};
Key features of the MTD context:
Create a modal component to display threat information to users:
// src/uniken/components/modals/ThreatDetectionModal.tsx
interface ThreatDetectionModalProps {
visible: boolean;
threats: RDNAThreatInfo[];
isConsentMode: boolean;
isProcessing?: boolean;
onProceed?: () => void;
onExit: () => void;
}
const ThreatDetectionModal: React.FC<ThreatDetectionModalProps> = ({
visible,
threats,
isConsentMode,
isProcessing = false,
onProceed,
onExit,
}) => {
const getThreatSeverityColor = (severity: string): string => {
switch (severity.toUpperCase()) {
case 'HIGH': return '#dc2626';
case 'MEDIUM': return '#f59e0b';
case 'LOW': return '#10b981';
default: return '#6b7280';
}
};
const renderThreatItem = (threat: RDNAThreatInfo, index: number) => (
<View key={threat.threatId} style={styles.threatItem}>
<Text style={styles.threatName}>{threat.threatName}</Text>
<Text style={styles.threatMessage}>{threat.threatMsg}</Text>
<View style={[styles.severityBadge,
{ backgroundColor: getThreatSeverityColor(threat.threatSeverity) }]}>
<Text style={styles.severityText}>{threat.threatSeverity}</Text>
</View>
</View>
);
return (
<Modal visible={visible} transparent={true} animationType="fade">
<View style={styles.modalOverlay}>
<View style={styles.modalContainer}>
<Text style={styles.modalTitle}>
{isConsentMode ? 'Security Threats Detected' : 'Critical Security Threat'}
</Text>
<ScrollView style={styles.threatsContainer}>
{threats.map((threat, index) => renderThreatItem(threat, index))}
</ScrollView>
<View style={styles.buttonContainer}>
{isConsentMode && onProceed && (
<TouchableOpacity
style={styles.proceedButton}
onPress={onProceed}
disabled={isProcessing}
>
<Text style={styles.buttonText}>
{isProcessing ? 'Processing...' : 'Proceed Anyway'}
</Text>
</TouchableOpacity>
)}
<TouchableOpacity
style={styles.exitButton}
onPress={onExit}
disabled={isProcessing}
>
<Text style={styles.buttonText}>Exit Application</Text>
</TouchableOpacity>
</View>
</View>
</View>
</Modal>
);
};
Key features of the threat detection modal:
The following image showcases screen from the sample application:
Wrap your application with the MTD context provider:
// App.tsx
import {MTDThreatProvider} from './src/uniken/MTDContext';
import { NavigationContainer } from '@react-navigation/native';
import { navigationRef } from './src/tutorial/navigation/NavigationService';
const AppContent = () => {
const {
isModalVisible,
threats,
isConsentMode,
isProcessing,
handleProceed,
handleExit,
} = useMTDThreat();
return (
<>
<NavigationContainer ref={navigationRef}>
{/* Your existing navigation including SecurityExitScreen */}
</NavigationContainer>
</>
);
};
function App() {
return (
<MTDThreatProvider>
<AppContent />
</MTDThreatProvider>
);
}
The context provider approach offers several advantages:
Threat Category | Examples | Typical Severity | Expected Response |
SYSTEM | Usb Debugging, Rooted Device | LOW-HIGH | User consent or termination |
NETWORK | Network MITM, Unsecured Access Point | LOW-MEDIUM | User consent or termination |
APP | Malware App, Repacked App | MEDIUM-HIGH | User consent or termination |
Use these debugging techniques to verify MTD functionality:
// Verify callback registration
console.log('MTD callbacks:', {
userConsent: !!eventManager.onUserConsentThreatsCallback,
terminate: !!eventManager.onTerminateWithThreatsCallback
});
// Log threat data
console.log('Received threats:', threats.map(t => ({
name: t.threatName,
severity: t.threatSeverity,
category: t.threatCategory
})));
Cause: MTD callbacks not properly registered Solution: Verify MTDThreatProvider
wraps your app and callbacks are set
Cause: Event listeners not attached Solution: Check that rdnaEmitter.addListener
calls are successful
Cause: Incorrect JSON format in takeActionOnThreats
call Solution: Ensure threat objects are properly serialized
// Correct format
const threatsJsonString = JSON.stringify(modifiedThreats);
console.log('Sending to API:', threatsJsonString);
Cause: Missing required threat properties Solution: Verify all required properties are present in threat objects
Cause: BackHandler.exitApp()
may not work as expected on all platforms
Solution: Use platform-appropriate exit handling
const handleExit = () => {
if (isConsentMode) {
// For consent threats, call takeActionOnThreats first
// This will trigger terminateWithThreats callback which handles exit
} else {
// For terminate threats, exit directly
BackHandler.exitApp();
}
};
Best Practice: Test exit behavior on both iOS and Android devices
Threat Severity | Recommended Action | User Choice |
LOW | Usually proceed with warning | User decides |
MEDIUM | Proceed with caution | User decides with strong warning |
HIGH | Consider termination | Limited or no user choice |
Congratulations! You've successfully learned how to implement comprehensive MTD functionality with:
Your MTD implementation now provides: