Skip to content

Latest commit

 

History

History
1422 lines (1143 loc) · 41.2 KB

File metadata and controls

1422 lines (1143 loc) · 41.2 KB

Permission Analysis Module - Final Implementation Plan

Last Updated: February 15, 2026
Status: 📝 READY TO IMPLEMENT
Approach: ML Category Classification + Rule-Based Risk Scoring
Priority: HIGH
Estimated Duration: 10-12 days


Table of Contents


Executive Summary

Goal

Build a Permission Analysis Module that:

  1. Classifies apps into categories based on their permissions
  2. Identifies unusual permissions for each category
  3. Calculates risk scores based on permission patterns
  4. Provides explainable results to users

Key Benefits

Lightweight - Only ~5MB model + 2KB mapping
Fast - <50ms inference per app
Accurate - 90-95% category classification accuracy
Explainable - Shows why apps are flagged as risky
Cached - Instant results after first analysis

What Makes This Different

NOT malware detection (already exists in the app)
Permission-based risk assessment - Analyzes if permissions are normal for app category


Architecture Overview

graph TD
    A[Installed App] --> B[Extract Permissions<br/>FeatureExtractor.kt]
    B --> C[Category Model<br/>ONNX/LGBM]
    C --> D[Predicted Category<br/>Social, Finance, etc.]
    
    B --> E[Actual Permissions]
    D --> F[Expected Permissions<br/>Hardcoded Mapping]
    
    E --> G[Compare Permissions]
    F --> G
    
    G --> H[Unusual Permissions]
    H --> I[Risk Scorer<br/>Rule-Based]
    I --> J[Risk Score 0.0-1.0]
    J --> K{Risk Level}
    
    K --> L[SAFE<br/>0.0-0.3]
    K --> M[SUSPICIOUS<br/>0.3-0.7]
    K --> N[HIGH-RISK<br/>0.7-1.0]
Loading

Data Flow

User Opens Permission Analyzer
    ↓
Get All Installed Apps
    ↓
For Each App:
    ↓
    Check Cache (Room Database)
    ↓
    Cache Valid? ──YES──> Return Cached Result
         │
         NO
         ↓
    Extract Permissions (FeatureExtractor)
         ↓
    Run Category Model (ONNX)
         ↓
    Get Expected Permissions (Hardcoded)
         ↓
    Compare & Calculate Risk (Rule-Based)
         ↓
    Save to Cache
         ↓
    Return Result
    ↓
Display Results to User

Phase 1: Dataset Collection

Duration: 2 days
Environment: Python
Output: CSV file with apps, categories, and permissions

Step 1.1: Download Google Play Store Dataset

Source: Kaggle
Link: https://www.kaggle.com/datasets/lava18/google-play-store-apps

Contains:

App,Category,Rating,Reviews,Size,Installs
WhatsApp Messenger,COMMUNICATION,4.4,69M,25M,1B+
Instagram,SOCIAL,4.5,66M,32M,1B+
PayPal,FINANCE,4.4,1M,89M,100M+
Candy Crush Saga,GAME,4.4,44M,74M,500M+

Download Script:

# Install Kaggle CLI
pip install kaggle

# Download dataset
kaggle datasets download -d lava18/google-play-store-apps

# Unzip
unzip google-play-store-apps.zip

Step 1.2: Filter Top Apps

File: scripts/filter_top_apps.py

import pandas as pd

# Load dataset
df = pd.read_csv('googleplaystore.csv')

# Clean data
df = df.dropna(subset=['App', 'Category', 'Installs'])

# Convert installs to numeric
df['Installs'] = df['Installs'].str.replace('+', '').str.replace(',', '').astype(int)

# Filter top 5000 apps by installs
top_apps = df.nlargest(5000, 'Installs')

# Map categories to our 10 categories
category_mapping = {
    'SOCIAL': 'Social',
    'COMMUNICATION': 'Communication',
    'FINANCE': 'Finance',
    'GAME': 'Games',
    'TOOLS': 'Utilities',
    'SHOPPING': 'Shopping',
    'ENTERTAINMENT': 'Entertainment',
    'PRODUCTIVITY': 'Productivity',
    'HEALTH_AND_FITNESS': 'Health',
    'EDUCATION': 'Education'
}

top_apps['Category'] = top_apps['Category'].map(category_mapping)
top_apps = top_apps.dropna(subset=['Category'])

# Save
top_apps[['App', 'Category']].to_csv('top_5000_apps.csv', index=False)
print(f"Filtered {len(top_apps)} apps")

Step 1.3: Scrape Permissions from Play Store

File: scripts/scrape_permissions.py

from google_play_scraper import app
import pandas as pd
import time

# Load filtered apps
apps_df = pd.read_csv('top_5000_apps.csv')

# Scrape permissions
data = []
for idx, row in apps_df.iterrows():
    try:
        # Get app details from Play Store
        app_id = row['App'].lower().replace(' ', '')
        result = app(
            app_id,
            lang='en',
            country='us'
        )
        
        permissions = result.get('permissions', [])
        
        data.append({
            'app_name': row['App'],
            'category': row['Category'],
            'permissions': ','.join(permissions)
        })
        
        print(f"[{idx+1}/{len(apps_df)}] Scraped: {row['App']}")
        time.sleep(0.5)  # Rate limiting
        
    except Exception as e:
        print(f"Error scraping {row['App']}: {e}")
        continue

# Save
df = pd.DataFrame(data)
df.to_csv('apps_with_permissions.csv', index=False)
print(f"\nScraped {len(df)} apps successfully")

Install Dependencies:

pip install google-play-scraper pandas

Phase 2: Data Preprocessing

Duration: 1 day
Environment: Python
Output: Training-ready dataset

Step 2.1: Clean and Normalize Permissions

File: scripts/preprocess_data.py

import pandas as pd
import numpy as np

# All Android permissions (145 permissions)
ALL_PERMISSIONS = [
    'ACCESS_BACKGROUND_LOCATION', 'ACCESS_COARSE_LOCATION', 'ACCESS_FINE_LOCATION',
    'ACCESS_NETWORK_STATE', 'ACCESS_WIFI_STATE', 'CAMERA', 'INTERNET',
    'READ_CONTACTS', 'WRITE_CONTACTS', 'READ_SMS', 'SEND_SMS', 'RECEIVE_SMS',
    'READ_CALL_LOG', 'WRITE_CALL_LOG', 'CALL_PHONE', 'READ_PHONE_STATE',
    'RECORD_AUDIO', 'WRITE_EXTERNAL_STORAGE', 'READ_EXTERNAL_STORAGE',
    'VIBRATE', 'WAKE_LOCK', 'USE_FINGERPRINT', 'USE_BIOMETRIC',
    # ... (add all 145 permissions)
]

def normalize_permission(perm):
    """Normalize permission to standard format"""
    if perm.startswith('android.permission.'):
        return perm.replace('android.permission.', '')
    return perm

def permissions_to_vector(permissions_str):
    """Convert comma-separated permissions to binary vector"""
    if pd.isna(permissions_str) or permissions_str == '':
        return [0] * len(ALL_PERMISSIONS)
    
    permissions = [normalize_permission(p.strip()) for p in permissions_str.split(',')]
    
    vector = []
    for perm in ALL_PERMISSIONS:
        vector.append(1 if perm in permissions else 0)
    
    return vector

# Load data
df = pd.read_csv('apps_with_permissions.csv')

# Convert permissions to vectors
print("Converting permissions to feature vectors...")
feature_vectors = df['permissions'].apply(permissions_to_vector)

# Create feature dataframe
X = pd.DataFrame(feature_vectors.tolist(), columns=ALL_PERMISSIONS)
y = df['category']

# Save
X.to_csv('X_features.csv', index=False)
y.to_csv('y_labels.csv', index=False)

print(f"Dataset shape: {X.shape}")
print(f"Categories: {y.value_counts()}")

Step 2.2: Split Train/Test Sets

from sklearn.model_selection import train_test_split

# Load features and labels
X = pd.read_csv('X_features.csv')
y = pd.read_csv('y_labels.csv')['category']

# Split 80/20
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# Save splits
X_train.to_csv('X_train.csv', index=False)
X_test.to_csv('X_test.csv', index=False)
y_train.to_csv('y_train.csv', index=False)
y_test.to_csv('y_test.csv', index=False)

print(f"Train set: {len(X_train)} samples")
print(f"Test set: {len(X_test)} samples")

Phase 3: Model Training

Duration: 2 days
Environment: Python, Jupyter Notebook
Output: Trained LGBM model

Step 3.1: Train LightGBM Model

File: scripts/train_model.py

import lightgbm as lgb
import pandas as pd
import numpy as np
from sklearn.metrics import classification_report, accuracy_score
from sklearn.preprocessing import LabelEncoder

# Load data
X_train = pd.read_csv('X_train.csv')
X_test = pd.read_csv('X_test.csv')
y_train = pd.read_csv('y_train.csv')['category']
y_test = pd.read_csv('y_test.csv')['category']

# Encode labels
le = LabelEncoder()
y_train_encoded = le.fit_transform(y_train)
y_test_encoded = le.transform(y_test)

# Create LightGBM datasets
train_data = lgb.Dataset(X_train, label=y_train_encoded)
test_data = lgb.Dataset(X_test, label=y_test_encoded, reference=train_data)

# Parameters optimized for permission classification
params = {
    'objective': 'multiclass',
    'num_class': len(le.classes_),
    'metric': 'multi_logloss',
    'boosting_type': 'gbdt',
    'num_leaves': 31,
    'learning_rate': 0.05,
    'feature_fraction': 0.9,
    'bagging_fraction': 0.8,
    'bagging_freq': 5,
    'verbose': 1,
    'max_depth': 10,
    'min_data_in_leaf': 20
}

# Train model
print("Training LightGBM model...")
model = lgb.train(
    params,
    train_data,
    num_boost_round=200,
    valid_sets=[test_data],
    callbacks=[
        lgb.early_stopping(stopping_rounds=20),
        lgb.log_evaluation(period=10)
    ]
)

# Evaluate
y_pred = model.predict(X_test)
y_pred_labels = np.argmax(y_pred, axis=1)
accuracy = accuracy_score(y_test_encoded, y_pred_labels)

print(f"\n✅ Model Accuracy: {accuracy * 100:.2f}%")
print("\nClassification Report:")
print(classification_report(y_test_encoded, y_pred_labels, target_names=le.classes_))

# Feature importance
feature_importance = pd.DataFrame({
    'feature': X_train.columns,
    'importance': model.feature_importance()
}).sort_values('importance', ascending=False)

print("\nTop 10 Most Important Permissions:")
print(feature_importance.head(10))

# Save model
model.save_model('category_classifier_lgbm.txt')

# Save label encoder
import pickle
with open('label_encoder.pkl', 'wb') as f:
    pickle.dump(le, f)

print("\n✅ Model saved to category_classifier_lgbm.txt")

Run Training:

python scripts/train_model.py

Expected Output:

Training LightGBM model...
[10]	valid_0's multi_logloss: 0.452
[20]	valid_0's multi_logloss: 0.389
...
[150]	valid_0's multi_logloss: 0.156

✅ Model Accuracy: 92.5%

Classification Report:
              precision    recall  f1-score   support
      Social       0.94      0.96      0.95       250
     Finance       0.91      0.89      0.90       180
       Games       0.95      0.93      0.94       320
   Utilities       0.88      0.91      0.89       150
...

Phase 4: ONNX Conversion

Duration: 1 day
Environment: Python
Output: category_model.onnx

Step 4.1: Convert LightGBM to ONNX

File: scripts/convert_to_onnx.py

import lightgbm as lgb
import onnxmltools
from onnxmltools.convert.common.data_types import FloatTensorType
import pickle

# Load trained model
model = lgb.Booster(model_file='category_classifier_lgbm.txt')

# Load label encoder
with open('label_encoder.pkl', 'rb') as f:
    le = pickle.load(f)

# Define input shape (145 permissions)
initial_types = [('input', FloatTensorType([None, 145]))]

# Convert to ONNX
print("Converting LightGBM to ONNX...")
onnx_model = onnxmltools.convert_lightgbm(
    model,
    initial_types=initial_types,
    target_opset=12
)

# Save ONNX model
onnx_model_path = 'category_model.onnx'
onnxmltools.utils.save_model(onnx_model, onnx_model_path)

print(f"✅ ONNX model saved to {onnx_model_path}")

# Get model size
import os
size_mb = os.path.getsize(onnx_model_path) / (1024 * 1024)
print(f"Model size: {size_mb:.2f} MB")

Install Dependencies:

pip install onnxmltools onnx onnxruntime

Run Conversion:

python scripts/convert_to_onnx.py

Step 4.2: Test ONNX Model

File: scripts/test_onnx.py

import onnxruntime as ort
import numpy as np
import pickle

# Load ONNX model
session = ort.InferenceSession('category_model.onnx')

# Load label encoder
with open('label_encoder.pkl', 'rb') as f:
    le = pickle.load(f)

# Test with sample input
test_permissions = np.zeros((1, 145), dtype=np.float32)
test_permissions[0, 0] = 1  # INTERNET
test_permissions[0, 5] = 1  # CAMERA
test_permissions[0, 7] = 1  # READ_CONTACTS

# Run inference
input_name = session.get_inputs()[0].name
output = session.run(None, {input_name: test_permissions})

# Get prediction
probabilities = output[0][0]
predicted_class = np.argmax(probabilities)
predicted_category = le.inverse_transform([predicted_class])[0]
confidence = probabilities[predicted_class]

print(f"Predicted Category: {predicted_category}")
print(f"Confidence: {confidence * 100:.2f}%")
print(f"\nAll Probabilities:")
for i, category in enumerate(le.classes_):
    print(f"  {category}: {probabilities[i] * 100:.2f}%")

Phase 5: Android Integration

Duration: 2 days
Environment: Android Studio, Kotlin
Output: Working ONNX inference on Android

Step 5.1: Add Dependencies

File: app/build.gradle.kts

dependencies {
    // ONNX Runtime
    implementation("com.microsoft.onnxruntime:onnxruntime-android:1.16.0")
    
    // Existing dependencies
    // ...
}

Step 5.2: Add Model to Assets

app/src/main/assets/
├── category_model.onnx (from Phase 4)
├── feature_order.txt (list of 145 permissions)
└── category_labels.txt (list of 10 categories)

File: app/src/main/assets/feature_order.txt

ACCESS_BACKGROUND_LOCATION
ACCESS_COARSE_LOCATION
ACCESS_FINE_LOCATION
...
(145 permissions total)

File: app/src/main/assets/category_labels.txt

Social
Finance
Games
Utilities
Communication
Shopping
Entertainment
Productivity
Health
Education

Step 5.3: Create CategoryOnnxScanner

File: app/src/main/java/com/droid/cybershield/core/inference/CategoryOnnxScanner.kt

package com.droid.cybershield.core.inference

import android.content.Context
import android.util.Log
import ai.onnxruntime.OnnxTensor
import ai.onnxruntime.OrtEnvironment
import ai.onnxruntime.OrtSession
import java.io.File
import java.nio.FloatBuffer
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class CategoryOnnxScanner private constructor(
    private val env: OrtEnvironment,
    private val session: OrtSession,
    private val featureOrder: List<String>,
    private val categories: List<String>
) {
    companion object {
        private const val TAG = "CategoryOnnxScanner"
        
        @Volatile
        private var instance: CategoryOnnxScanner? = null
        
        fun get(ctx: Context): CategoryOnnxScanner = instance ?: synchronized(this) {
            instance ?: try {
                val env = OrtEnvironment.getEnvironment()
                
                // Load model
                val model = File(ctx.cacheDir, "category_model.onnx")
                if (!model.exists()) {
                    ctx.assets.open("category_model.onnx").use { input ->
                        model.outputStream().use { output ->
                            input.copyTo(output)
                        }
                    }
                }
                
                val session = env.createSession(model.absolutePath)
                
                // Load feature order
                val featureOrder = ctx.assets.open("feature_order.txt").use {
                    it.bufferedReader().readLines().map { line -> line.trim() }
                }
                
                // Load categories
                val categories = ctx.assets.open("category_labels.txt").use {
                    it.bufferedReader().readLines().map { line -> line.trim() }
                }
                
                Log.d(TAG, "Category Scanner initialized")
                CategoryOnnxScanner(env, session, featureOrder, categories).also { instance = it }
            } catch (e: Exception) {
                Log.e(TAG, "Failed to initialize Category Scanner", e)
                throw RuntimeException("Failed to initialize: ${e.message}", e)
            }
        }
    }
    
    suspend fun predictCategory(payload: Map<String, Any>): CategoryPrediction {
        return try {
            val n = featureOrder.size
            val arr = FloatArray(n)
            
            // Convert payload to feature array
            for (i in 0 until n) {
                val k = featureOrder[i]
                arr[i] = when (val v = payload[k]) {
                    is Int -> v.toFloat()
                    is Boolean -> if (v) 1f else 0f
                    null -> 0f
                    else -> 0f
                }
            }
            
            // Run inference
            val inputName = session.inputNames.first()
            val result = OnnxTensor.createTensor(env, FloatBuffer.wrap(arr), longArrayOf(1, n.toLong())).use { tensor ->
                session.run(mapOf(inputName to tensor)).use { output ->
                    extractCategory(output[0].value)
                }
            }
            
            result
        } catch (e: Exception) {
            Log.e(TAG, "Prediction failed", e)
            CategoryPrediction("Utilities", 0.5f)
        }
    }
    
    private fun extractCategory(output: Any?): CategoryPrediction {
        when (output) {
            is FloatArray -> {
                val maxIndex = output.indices.maxByOrNull { output[it] } ?: 0
                return CategoryPrediction(
                    category = categories[maxIndex],
                    confidence = output[maxIndex]
                )
            }
            is Array<*> -> {
                if (output.isNotEmpty()) {
                    return extractCategory(output[0])
                }
            }
        }
        return CategoryPrediction("Utilities", 0.5f)
    }
}

data class CategoryPrediction(
    val category: String,
    val confidence: Float
)

Phase 6: Rule-Based Risk Scoring

Duration: 1 day
Environment: Kotlin
Output: Risk scoring logic

Step 6.1: Create Category Permission Mapping

File: app/src/main/java/com/droid/cybershield/core/permission/CategoryPermissions.kt

package com.droid.cybershield.core.permission

object CategoryPermissions {
    
    private val CATEGORY_EXPECTED_PERMISSIONS = mapOf(
        "Social" to setOf(
            "android.permission.INTERNET",
            "android.permission.CAMERA",
            "android.permission.READ_CONTACTS",
            "android.permission.RECORD_AUDIO",
            "android.permission.ACCESS_FINE_LOCATION",
            "android.permission.WRITE_EXTERNAL_STORAGE",
            "android.permission.VIBRATE"
        ),
        "Finance" to setOf(
            "android.permission.INTERNET",
            "android.permission.CAMERA",
            "android.permission.ACCESS_FINE_LOCATION",
            "android.permission.READ_PHONE_STATE",
            "android.permission.USE_FINGERPRINT",
            "android.permission.USE_BIOMETRIC"
        ),
        "Games" to setOf(
            "android.permission.INTERNET",
            "android.permission.ACCESS_NETWORK_STATE",
            "android.permission.VIBRATE",
            "android.permission.WRITE_EXTERNAL_STORAGE",
            "android.permission.WAKE_LOCK"
        ),
        "Utilities" to setOf(
            "android.permission.INTERNET",
            "android.permission.WRITE_EXTERNAL_STORAGE",
            "android.permission.ACCESS_NETWORK_STATE",
            "android.permission.WAKE_LOCK"
        ),
        "Communication" to setOf(
            "android.permission.INTERNET",
            "android.permission.READ_SMS",
            "android.permission.SEND_SMS",
            "android.permission.READ_CONTACTS",
            "android.permission.CALL_PHONE",
            "android.permission.CAMERA",
            "android.permission.RECORD_AUDIO",
            "android.permission.READ_CALL_LOG"
        ),
        "Shopping" to setOf(
            "android.permission.INTERNET",
            "android.permission.CAMERA",
            "android.permission.ACCESS_FINE_LOCATION",
            "android.permission.VIBRATE"
        ),
        "Entertainment" to setOf(
            "android.permission.INTERNET",
            "android.permission.ACCESS_NETWORK_STATE",
            "android.permission.WRITE_EXTERNAL_STORAGE",
            "android.permission.WAKE_LOCK"
        ),
        "Productivity" to setOf(
            "android.permission.INTERNET",
            "android.permission.WRITE_EXTERNAL_STORAGE",
            "android.permission.CAMERA",
            "android.permission.READ_CALENDAR",
            "android.permission.WRITE_CALENDAR"
        ),
        "Health" to setOf(
            "android.permission.INTERNET",
            "android.permission.BODY_SENSORS",
            "android.permission.ACTIVITY_RECOGNITION",
            "android.permission.ACCESS_FINE_LOCATION",
            "android.permission.CAMERA"
        ),
        "Education" to setOf(
            "android.permission.INTERNET",
            "android.permission.CAMERA",
            "android.permission.RECORD_AUDIO",
            "android.permission.WRITE_EXTERNAL_STORAGE"
        )
    )
    
    private val DANGEROUS_PERMISSIONS = setOf(
        "android.permission.READ_SMS",
        "android.permission.SEND_SMS",
        "android.permission.RECEIVE_SMS",
        "android.permission.READ_CONTACTS",
        "android.permission.WRITE_CONTACTS",
        "android.permission.ACCESS_FINE_LOCATION",
        "android.permission.CAMERA",
        "android.permission.RECORD_AUDIO",
        "android.permission.READ_CALL_LOG",
        "android.permission.WRITE_CALL_LOG",
        "android.permission.CALL_PHONE",
        "android.permission.READ_PHONE_STATE"
    )
    
    fun getExpectedPermissions(category: String): Set<String> {
        return CATEGORY_EXPECTED_PERMISSIONS[category] ?: emptySet()
    }
    
    fun isDangerous(permission: String): Boolean {
        return permission in DANGEROUS_PERMISSIONS
    }
}

Step 6.2: Create Risk Scorer

File: app/src/main/java/com/droid/cybershield/core/permission/RiskScorer.kt

package com.droid.cybershield.core.permission

import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class RiskScorer @Inject constructor() {
    
    fun calculateRiskScore(
        permissions: List<String>,
        category: String
    ): RiskAssessment {
        val reasons = mutableListOf<String>()
        var riskScore = 0f
        
        // Get expected permissions for category
        val expectedPerms = CategoryPermissions.getExpectedPermissions(category)
        
        // Find unusual permissions
        val unusualPerms = permissions.filter { it !in expectedPerms }
        
        // Rule 1: Unusual permissions
        if (unusualPerms.isNotEmpty()) {
            val unusualScore = (unusualPerms.size * 0.15f).coerceAtMost(0.6f)
            riskScore += unusualScore
            reasons.add("${unusualPerms.size} unusual permission(s) for $category apps")
        }
        
        // Rule 2: Dangerous permissions
        val dangerousPerms = permissions.filter { CategoryPermissions.isDangerous(it) }
        if (dangerousPerms.isNotEmpty()) {
            val dangerousScore = (dangerousPerms.size * 0.05f).coerceAtMost(0.3f)
            riskScore += dangerousScore
            reasons.add("${dangerousPerms.size} dangerous permission(s)")
        }
        
        // Rule 3: High-risk combinations
        if (hasHighRiskCombination(permissions)) {
            riskScore += 0.3f
            reasons.add("Suspicious permission combination detected")
        }
        
        // Clamp score
        riskScore = riskScore.coerceIn(0f, 1f)
        
        return RiskAssessment(
            score = riskScore,
            level = classifyRiskLevel(riskScore),
            reasons = reasons,
            unusualPermissions = unusualPerms
        )
    }
    
    private fun hasHighRiskCombination(permissions: List<String>): Boolean {
        val hasSMS = permissions.any { it.contains("SMS") }
        val hasLocation = permissions.any { it.contains("LOCATION") }
        val hasInternet = "android.permission.INTERNET" in permissions
        
        return hasSMS && hasLocation && hasInternet
    }
    
    private fun classifyRiskLevel(score: Float): RiskLevel {
        return when {
            score < 0.3f -> RiskLevel.SAFE
            score < 0.7f -> RiskLevel.SUSPICIOUS
            else -> RiskLevel.HIGH_RISK
        }
    }
}

data class RiskAssessment(
    val score: Float,
    val level: RiskLevel,
    val reasons: List<String>,
    val unusualPermissions: List<String>
)

enum class RiskLevel {
    SAFE,
    SUSPICIOUS,
    HIGH_RISK
}

Phase 7: Caching Implementation

Duration: 1 day
Environment: Kotlin, Room
Output: Cached analysis results

Step 7.1: Create Cache Entity

File: app/src/main/java/com/droid/cybershield/data/local/entity/PermissionAnalysisCacheEntity.kt

package com.droid.cybershield.data.local.entity

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "permission_analysis_cache")
data class PermissionAnalysisCacheEntity(
    @PrimaryKey
    val packageName: String,
    val appVersion: Long,
    val category: String,
    val categoryConfidence: Float,
    val riskScore: Float,
    val riskLevel: String,
    val reasons: String,              // JSON array
    val analyzedAt: Long,
    val permissionsHash: String
)

Step 7.2: Create DAO

File: app/src/main/java/com/droid/cybershield/data/local/dao/PermissionAnalysisCacheDao.kt

package com.droid.cybershield.data.local.dao

import androidx.room.*
import com.droid.cybershield.data.local.entity.PermissionAnalysisCacheEntity

@Dao
interface PermissionAnalysisCacheDao {
    
    @Query("SELECT * FROM permission_analysis_cache WHERE packageName = :packageName")
    suspend fun getCache(packageName: String): PermissionAnalysisCacheEntity?
    
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun saveCache(cache: PermissionAnalysisCacheEntity)
    
    @Query("SELECT * FROM permission_analysis_cache")
    suspend fun getAllCached(): List<PermissionAnalysisCacheEntity>
    
    @Query("DELETE FROM permission_analysis_cache WHERE packageName = :packageName")
    suspend fun deleteCache(packageName: String)
    
    @Query("DELETE FROM permission_analysis_cache")
    suspend fun clearAll()
}

Step 7.3: Update Database

File: app/src/main/java/com/droid/cybershield/data/local/CyberShieldDatabase.kt

@Database(
    entities = [
        MalwareCacheEntity::class,
        PermissionAnalysisCacheEntity::class  // ADD THIS
    ],
    version = 2  // Increment version
)
abstract class CyberShieldDatabase : RoomDatabase() {
    abstract fun malwareCacheDao(): MalwareCacheDao
    abstract fun permissionAnalysisCacheDao(): PermissionAnalysisCacheDao  // ADD THIS
}

Step 7.4: Create Permission Analyzer with Caching

File: app/src/main/java/com/droid/cybershield/core/permission/PermissionAnalyzer.kt

package com.droid.cybershield.core.permission

import android.content.Context
import android.content.pm.PackageManager
import android.util.Log
import com.droid.cybershield.core.features.FeatureExtractor
import com.droid.cybershield.core.features.toPayload
import com.droid.cybershield.core.inference.CategoryOnnxScanner
import com.droid.cybershield.data.local.dao.PermissionAnalysisCacheDao
import com.droid.cybershield.data.local.entity.PermissionAnalysisCacheEntity
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONArray
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class PermissionAnalyzer @Inject constructor(
    private val context: Context,
    private val cacheDao: PermissionAnalysisCacheDao,
    private val riskScorer: RiskScorer
) {
    
    suspend fun analyzeApp(packageName: String): PermissionAnalysisResult = withContext(Dispatchers.Default) {
        try {
            // Get app info
            val packageInfo = context.packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS)
            val currentVersion = packageInfo.longVersionCode
            val permissions = packageInfo.requestedPermissions?.toList() ?: emptyList()
            
            // Check cache
            val cached = cacheDao.getCache(packageName)
            if (cached != null && !shouldReAnalyze(cached, currentVersion, permissions)) {
                Log.d(TAG, "Using cached result for $packageName")
                return@withContext cached.toResult()
            }
            
            // Perform analysis
            Log.d(TAG, "Analyzing $packageName")
            
            val features = FeatureExtractor.fromInstalled(context, packageName)
            val payload = features.toPayload()
            
            // Predict category
            val categoryPrediction = CategoryOnnxScanner.get(context).predictCategory(payload)
            
            // Calculate risk
            val riskAssessment = riskScorer.calculateRiskScore(permissions, categoryPrediction.category)
            
            val result = PermissionAnalysisResult(
                packageName = packageName,
                appName = getAppName(packageName),
                category = categoryPrediction.category,
                categoryConfidence = categoryPrediction.confidence,
                riskScore = riskAssessment.score,
                riskLevel = riskAssessment.level,
                reasons = riskAssessment.reasons,
                totalPermissions = permissions.size,
                unusualPermissions = riskAssessment.unusualPermissions
            )
            
            // Save to cache
            cacheDao.saveCache(result.toCacheEntity(currentVersion, permissions))
            
            result
        } catch (e: Exception) {
            Log.e(TAG, "Error analyzing $packageName", e)
            PermissionAnalysisResult.error(packageName)
        }
    }
    
    private fun shouldReAnalyze(
        cached: PermissionAnalysisCacheEntity,
        currentVersion: Long,
        currentPermissions: List<String>
    ): Boolean {
        if (cached.appVersion != currentVersion) return true
        
        val currentHash = hashPermissions(currentPermissions)
        if (cached.permissionsHash != currentHash) return true
        
        val cacheAge = System.currentTimeMillis() - cached.analyzedAt
        if (cacheAge > 30L * 24 * 60 * 60 * 1000) return true
        
        return false
    }
    
    private fun hashPermissions(permissions: List<String>): String {
        return permissions.sorted().joinToString(",").hashCode().toString()
    }
    
    private fun getAppName(packageName: String): String {
        return try {
            val appInfo = context.packageManager.getApplicationInfo(packageName, 0)
            context.packageManager.getApplicationLabel(appInfo).toString()
        } catch (e: Exception) {
            packageName
        }
    }
    
    companion object {
        private const val TAG = "PermissionAnalyzer"
    }
}

data class PermissionAnalysisResult(
    val packageName: String,
    val appName: String,
    val category: String,
    val categoryConfidence: Float,
    val riskScore: Float,
    val riskLevel: RiskLevel,
    val reasons: List<String>,
    val totalPermissions: Int,
    val unusualPermissions: List<String>
) {
    fun toCacheEntity(version: Long, permissions: List<String>): PermissionAnalysisCacheEntity {
        return PermissionAnalysisCacheEntity(
            packageName = packageName,
            appVersion = version,
            category = category,
            categoryConfidence = categoryConfidence,
            riskScore = riskScore,
            riskLevel = riskLevel.name,
            reasons = JSONArray(reasons).toString(),
            analyzedAt = System.currentTimeMillis(),
            permissionsHash = permissions.sorted().joinToString(",").hashCode().toString()
        )
    }
    
    companion object {
        fun error(packageName: String) = PermissionAnalysisResult(
            packageName = packageName,
            appName = "Unknown",
            category = "Unknown",
            categoryConfidence = 0f,
            riskScore = 0.5f,
            riskLevel = RiskLevel.SUSPICIOUS,
            reasons = listOf("Error analyzing app"),
            totalPermissions = 0,
            unusualPermissions = emptyList()
        )
    }
}

fun PermissionAnalysisCacheEntity.toResult(): PermissionAnalysisResult {
    val reasonsList = JSONArray(reasons).let { arr ->
        (0 until arr.length()).map { arr.getString(it) }
    }
    
    return PermissionAnalysisResult(
        packageName = packageName,
        appName = packageName,
        category = category,
        categoryConfidence = categoryConfidence,
        riskScore = riskScore,
        riskLevel = RiskLevel.valueOf(riskLevel),
        reasons = reasonsList,
        totalPermissions = 0,
        unusualPermissions = emptyList()
    )
}

Phase 8: UI Implementation

Duration: 2 days
Environment: Jetpack Compose
Output: Permission Analysis screen

Step 8.1: Create ViewModel

File: app/src/main/java/com/droid/cybershield/presentation/permission/PermissionAnalysisViewModel.kt

package com.droid.cybershield.presentation.permission

import android.content.Context
import android.content.pm.PackageManager
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.droid.cybershield.core.permission.PermissionAnalyzer
import com.droid.cybershield.core.permission.PermissionAnalysisResult
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class PermissionAnalysisViewModel @Inject constructor(
    @ApplicationContext private val context: Context,
    private val permissionAnalyzer: PermissionAnalyzer
) : ViewModel() {
    
    private val _uiState = MutableStateFlow<PermissionAnalysisUiState>(PermissionAnalysisUiState.Idle)
    val uiState: StateFlow<PermissionAnalysisUiState> = _uiState.asStateFlow()
    
    fun scanAllApps() {
        viewModelScope.launch {
            _uiState.value = PermissionAnalysisUiState.Scanning(0, 0)
            
            val packageManager = context.packageManager
            val installedApps = packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
            val total = installedApps.size
            
            val results = mutableListOf<PermissionAnalysisResult>()
            
            installedApps.forEachIndexed { index, appInfo ->
                val result = permissionAnalyzer.analyzeApp(appInfo.packageName)
                results.add(result)
                
                _uiState.value = PermissionAnalysisUiState.Scanning(index + 1, total)
            }
            
            _uiState.value = PermissionAnalysisUiState.Success(results.sortedByDescending { it.riskScore })
        }
    }
}

sealed class PermissionAnalysisUiState {
    object Idle : PermissionAnalysisUiState()
    data class Scanning(val current: Int, val total: Int) : PermissionAnalysisUiState()
    data class Success(val results: List<PermissionAnalysisResult>) : PermissionAnalysisUiState()
}

Step 8.2: Create Composable UI

File: app/src/main/java/com/droid/cybershield/presentation/permission/PermissionAnalysisView.kt

package com.droid.cybershield.presentation.permission

import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.droid.cybershield.core.permission.PermissionAnalysisResult
import com.droid.cybershield.core.permission.RiskLevel

@Composable
fun PermissionAnalysisView(
    viewModel: PermissionAnalysisViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState.collectAsState()
    
    Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
        Text("Permission Analysis", style = MaterialTheme.typography.headlineMedium)
        
        Spacer(modifier = Modifier.height(16.dp))
        
        Button(
            onClick = { viewModel.scanAllApps() },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(
                when (uiState) {
                    is PermissionAnalysisUiState.Scanning -> "Scanning..."
                    else -> "Scan All Apps"
                }
            )
        }
        
        Spacer(modifier = Modifier.height(16.dp))
        
        when (val state = uiState) {
            is PermissionAnalysisUiState.Idle -> {
                Text("Tap 'Scan All Apps' to analyze permissions")
            }
            is PermissionAnalysisUiState.Scanning -> {
                LinearProgressIndicator(
                    progress = state.current.toFloat() / state.total,
                    modifier = Modifier.fillMaxWidth()
                )
                Text("Analyzing ${state.current} / ${state.total}")
            }
            is PermissionAnalysisUiState.Success -> {
                ResultsList(results = state.results)
            }
        }
    }
}

@Composable
fun ResultsList(results: List<PermissionAnalysisResult>) {
    LazyColumn {
        items(results) { result ->
            PermissionAnalysisCard(result)
        }
    }
}

@Composable
fun PermissionAnalysisCard(result: PermissionAnalysisResult) {
    Card(
        modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
        colors = CardDefaults.cardColors(
            containerColor = when (result.riskLevel) {
                RiskLevel.SAFE -> Color.Green.copy(alpha = 0.1f)
                RiskLevel.SUSPICIOUS -> Color.Yellow.copy(alpha = 0.1f)
                RiskLevel.HIGH_RISK -> Color.Red.copy(alpha = 0.1f)
            }
        )
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(result.appName, style = MaterialTheme.typography.titleMedium)
            Text("Category: ${result.category} (${(result.categoryConfidence * 100).toInt()}%)")
            Text("Risk: ${result.riskLevel}")
            Text("Score: ${(result.riskScore * 100).toInt()}%")
            
            if (result.reasons.isNotEmpty()) {
                Spacer(modifier = Modifier.height(8.dp))
                Text("Reasons:", style = MaterialTheme.typography.labelSmall)
                result.reasons.forEach { reason ->
                    Text("$reason", style = MaterialTheme.typography.bodySmall)
                }
            }
        }
    }
}

Phase 9: Testing

Duration: 1 day
Environment: JUnit, Android Test
Output: Test coverage

Step 9.1: Unit Tests

File: app/src/test/java/com/droid/cybershield/core/permission/RiskScorerTest.kt

class RiskScorerTest {
    private val riskScorer = RiskScorer()
    
    @Test
    fun `flashlight app with SMS should be high risk`() {
        val permissions = listOf(
            "android.permission.INTERNET",
            "android.permission.READ_SMS",
            "android.permission.SEND_SMS",
            "android.permission.ACCESS_FINE_LOCATION"
        )
        
        val result = riskScorer.calculateRiskScore(permissions, "Utilities")
        
        assertTrue(result.score > 0.7f)
        assertEquals(RiskLevel.HIGH_RISK, result.level)
    }
    
    @Test
    fun `social app with normal permissions should be safe`() {
        val permissions = listOf(
            "android.permission.INTERNET",
            "android.permission.CAMERA",
            "android.permission.READ_CONTACTS"
        )
        
        val result = riskScorer.calculateRiskScore(permissions, "Social")
        
        assertTrue(result.score < 0.3f)
        assertEquals(RiskLevel.SAFE, result.level)
    }
}

Verification Plan

Manual Testing

  1. Install app on device
  2. Navigate to Permission Analysis
  3. Tap "Scan All Apps"
  4. Verify results:
    • System apps → SAFE
    • WhatsApp → SAFE (Social category)
    • Banking apps → SAFE (Finance category)
    • Test malicious app → HIGH-RISK

Expected Results

App Category Risk Level Reason
Chrome Utilities SAFE Normal permissions
WhatsApp Social SAFE Expected permissions
PayPal Finance SAFE Normal for finance
Test Malware Utilities HIGH-RISK Unusual permissions

Summary

Total Implementation Time: 10-12 days

Components:

  • Category classification model (ONNX/LGBM)
  • Rule-based risk scorer
  • Caching system (Room)
  • UI (Jetpack Compose)

App Size Impact: ~5MB (model only)

Performance: <50ms per app (after caching: <1ms)

Accuracy: 90-95% category classification

Status: ✅ READY FOR IMPLEMENTATION