Spaces:
Running
Running
| import 'dart:io'; | |
| import 'dart:math'; | |
| import '../models/kavacha_models.dart'; | |
| class Layer5PayloadAnalyzer implements KavachaLayer { | |
| static const List<String> officialDomains = [ | |
| 'sbi.co.in', | |
| 'onlinesbi.sbi', | |
| 'hdfcbank.com', | |
| 'icicibank.com', | |
| 'axisbank.com', | |
| 'incometax.gov.in', | |
| 'indiapost.gov.in', | |
| 'pnbindia.in', | |
| 'epfindia.gov.in', | |
| 'irctc.co.in' | |
| ]; | |
| Future<LayerResult> inspect(SmsContext ctx) async { | |
| final urls = _extractUrls(ctx.body); | |
| if (urls.isEmpty) { | |
| return LayerResult.pass(); | |
| } | |
| for (String url in urls) { | |
| final domain = _extractDomain(url); | |
| if (domain.isEmpty) continue; | |
| // 1. Squatter Check | |
| for (String official in officialDomains) { | |
| if (domain == official || domain.endsWith('.\$official')) { | |
| // Exact match, trusted domain | |
| continue; | |
| } | |
| int distance = _levenshtein(domain, official); | |
| // If it's suspiciously close (distance 1 or 2) but NOT an exact match | |
| if (distance > 0 && distance <= 2) { | |
| return LayerResult.block( | |
| "Domain Squatting Detected", 0.98, ["payload_squatter:\$domain(sim:\$official)"], 5); | |
| } | |
| } | |
| // 2. Payload Check | |
| bool isApk = await _isApkPayload(url); | |
| if (isApk) { | |
| return LayerResult.block("Malicious APK Payload", 0.99, ["payload_apk:\$url"], 5); | |
| } | |
| } | |
| return LayerResult.pass(); | |
| } | |
| List<String> _extractUrls(String text) { | |
| RegExp exp = RegExp( | |
| r"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)", | |
| caseSensitive: false); | |
| return exp.allMatches(text).map((m) => m.group(0)!).toList(); | |
| } | |
| String _extractDomain(String urlString) { | |
| try { | |
| return Uri.parse(urlString).host.toLowerCase(); | |
| } catch (e) { | |
| return ""; | |
| } | |
| } | |
| Future<bool> _isApkPayload(String urlString) async { | |
| if (urlString.toLowerCase().endsWith('.apk')) return true; | |
| // Attempt lightweight HEAD request for hidden payloads | |
| try { | |
| final client = HttpClient(); | |
| client.connectionTimeout = const Duration(seconds: 3); | |
| final request = await client.headUrl(Uri.parse(urlString)); | |
| request.followRedirects = true; | |
| request.maxRedirects = 3; | |
| final response = await request.close(); | |
| final contentType = response.headers.value(HttpHeaders.contentTypeHeader); | |
| if (contentType != null && contentType.contains('application/vnd.android.package-archive')) { | |
| return true; | |
| } | |
| } catch (e) { | |
| // Ignore network timeouts silently | |
| } | |
| return false; | |
| } | |
| int _levenshtein(String a, String b) { | |
| if (a.isEmpty) return b.length; | |
| if (b.isEmpty) return a.length; | |
| List<int> costs = List.generate(b.length + 1, (i) => i); | |
| for (int i = 0; i < a.length; i++) { | |
| int lastValue = i + 1; | |
| for (int j = 0; j < b.length; j++) { | |
| int newValue = costs[j] + (a[i] == b[j] ? 0 : 1); | |
| if (newValue > lastValue + 1) newValue = lastValue + 1; | |
| if (newValue > costs[j + 1] + 1) newValue = costs[j + 1] + 1; | |
| costs[j] = lastValue; | |
| lastValue = newValue; | |
| } | |
| costs[b.length] = lastValue; | |
| } | |
| return costs[b.length]; | |
| } | |
| } | |