Last Updated: February 15, 2026
Status: 📝 READY TO IMPLEMENT
Approach: ML Category Classification + Rule-Based Risk Scoring
Priority: HIGH
Estimated Duration: 10-12 days
- Executive Summary
- Architecture Overview
- Phase 1: Dataset Collection
- Phase 2: Data Preprocessing
- Phase 3: Model Training
- Phase 4: ONNX Conversion
- Phase 5: Android Integration
- Phase 6: Rule-Based Risk Scoring
- Phase 7: Caching Implementation
- Phase 8: UI Implementation
- Phase 9: Testing
- Verification Plan
Build a Permission Analysis Module that:
- Classifies apps into categories based on their permissions
- Identifies unusual permissions for each category
- Calculates risk scores based on permission patterns
- Provides explainable results to users
✅ 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
NOT malware detection (already exists in the app)
Permission-based risk assessment - Analyzes if permissions are normal for app category
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]
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
Duration: 2 days
Environment: Python
Output: CSV file with apps, categories, and permissions
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.zipFile: 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")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 pandasDuration: 1 day
Environment: Python
Output: Training-ready dataset
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()}")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")Duration: 2 days
Environment: Python, Jupyter Notebook
Output: Trained LGBM 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.pyExpected 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
...
Duration: 1 day
Environment: Python
Output: category_model.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 onnxruntimeRun Conversion:
python scripts/convert_to_onnx.pyFile: 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}%")Duration: 2 days
Environment: Android Studio, Kotlin
Output: Working ONNX inference on Android
File: app/build.gradle.kts
dependencies {
// ONNX Runtime
implementation("com.microsoft.onnxruntime:onnxruntime-android:1.16.0")
// Existing dependencies
// ...
}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
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
)Duration: 1 day
Environment: Kotlin
Output: Risk scoring logic
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
}
}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
}Duration: 1 day
Environment: Kotlin, Room
Output: Cached analysis results
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
)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()
}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
}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()
)
}Duration: 2 days
Environment: Jetpack Compose
Output: Permission Analysis screen
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()
}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)
}
}
}
}
}Duration: 1 day
Environment: JUnit, Android Test
Output: Test coverage
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)
}
}- Install app on device
- Navigate to Permission Analysis
- Tap "Scan All Apps"
- Verify results:
- System apps → SAFE
- WhatsApp → SAFE (Social category)
- Banking apps → SAFE (Finance category)
- Test malicious app → HIGH-RISK
| App | Category | Risk Level | Reason |
|---|---|---|---|
| Chrome | Utilities | SAFE | Normal permissions |
| Social | SAFE | Expected permissions | |
| PayPal | Finance | SAFE | Normal for finance |
| Test Malware | Utilities | HIGH-RISK | Unusual permissions |
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