1→import Foundation 2→ 3→/// Manages loading and checking blocklists for domains and apps 4→class BlocklistManager { 5→ static let shared = BlocklistManager() 6→ 7→ // MARK: - Cached Data 8→ 9→ private var adultDomains: Set = [] 10→ private var adultKeywords: [String] = [] 11→ 12→ private var drugDomains: Set = [] 13→ private var drugKeywords: [String] = [] 14→ private var drugSubreddits: Set = [] 15→ 16→ private var gamblingDomains: Set = [] 17→ private var gamblingKeywords: [String] = [] 18→ 19→ private var riskyAppBundleIds: Set = [] 20→ private var riskyAppKeywords: [String] = [] 21→ 22→ private var isLoaded = false 23→ 24→ private init() { 25→ loadBlocklists() 26→ } 27→ 28→ // MARK: - Loading 29→ 30→ /// Load all blocklists from JSON files 31→ func loadBlocklists() { 32→ loadAdultDomains() 33→ loadDrugDomains() 34→ loadGamblingDomains() 35→ loadRiskyApps() 36→ isLoaded = true 37→ print("BlocklistManager: Loaded all blocklists") 38→ } 39→ 40→ private func loadAdultDomains() { 41→ guard let url = Bundle.main.url(forResource: "adult-domains", withExtension: "json", subdirectory: "Blocklists"), 42→ let data = try? Data(contentsOf: url), 43→ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { 44→ print("BlocklistManager: Failed to load adult-domains.json, using defaults") 45→ loadDefaultAdultDomains() 46→ return 47→ } 48→ 49→ if let domains = json["domains"] as? [String] { 50→ adultDomains = Set(domains.map { $0.lowercased() }) 51→ } 52→ if let keywords = json["keywords"] as? [String] { 53→ adultKeywords = keywords.map { $0.lowercased() } 54→ } 55→ 56→ print("BlocklistManager: Loaded \(adultDomains.count) adult domains, \(adultKeywords.count) keywords") 57→ } 58→ 59→ private func loadDrugDomains() { 60→ guard let url = Bundle.main.url(forResource: "drug-domains", withExtension: "json", subdirectory: "Blocklists"), 61→ let data = try? Data(contentsOf: url), 62→ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { 63→ print("BlocklistManager: Failed to load drug-domains.json, using defaults") 64→ loadDefaultDrugDomains() 65→ return 66→ } 67→ 68→ if let domains = json["domains"] as? [String] { 69→ drugDomains = Set(domains.map { $0.lowercased() }) 70→ } 71→ if let keywords = json["keywords"] as? [String] { 72→ drugKeywords = keywords.map { $0.lowercased() } 73→ } 74→ if let subreddits = json["subreddits_to_block"] as? [String] { 75→ drugSubreddits = Set(subreddits.map { $0.lowercased() }) 76→ } 77→ 78→ print("BlocklistManager: Loaded \(drugDomains.count) drug domains, \(drugKeywords.count) keywords") 79→ } 80→ 81→ private func loadGamblingDomains() { 82→ guard let url = Bundle.main.url(forResource: "gambling-domains", withExtension: "json", subdirectory: "Blocklists"), 83→ let data = try? Data(contentsOf: url), 84→ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { 85→ print("BlocklistManager: Failed to load gambling-domains.json, using defaults") 86→ loadDefaultGamblingDomains() 87→ return 88→ } 89→ 90→ if let domains = json["domains"] as? [String] { 91→ gamblingDomains = Set(domains.map { $0.lowercased() }) 92→ } 93→ if let keywords = json["keywords"] as? [String] { 94→ gamblingKeywords = keywords.map { $0.lowercased() } 95→ } 96→ 97→ print("BlocklistManager: Loaded \(gamblingDomains.count) gambling domains, \(gamblingKeywords.count) keywords") 98→ } 99→ 100→ private func loadRiskyApps() { 101→ guard let url = Bundle.main.url(forResource: "risky-apps", withExtension: "json", subdirectory: "Blocklists"), 102→ let data = try? Data(contentsOf: url), 103→ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { 104→ print("BlocklistManager: Failed to load risky-apps.json, using defaults") 105→ loadDefaultRiskyApps() 106→ return 107→ } 108→ 109→ var bundleIds: Set = [] 110→ 111→ // Extract bundle IDs from all categories 112→ for category in ["vpn_apps", "anonymous_browsers", "risky_messaging", "adult_apps"] { 113→ if let apps = json[category] as? [[String: Any]] { 114→ for app in apps { 115→ if let bundleId = app["bundleId"] as? String { 116→ bundleIds.insert(bundleId.lowercased()) 117→ } 118→ } 119→ } 120→ } 121→ 122→ riskyAppBundleIds = bundleIds 123→ 124→ if let keywords = json["keywords_in_app_names"] as? [String] { 125→ riskyAppKeywords = keywords.map { $0.lowercased() } 126→ } 127→ 128→ print("BlocklistManager: Loaded \(riskyAppBundleIds.count) risky app bundle IDs, \(riskyAppKeywords.count) keywords") 129→ } 130→ 131→ // MARK: - Default Data (Fallback) 132→ 133→ private func loadDefaultAdultDomains() { 134→ adultDomains = Set([ 135→ "pornhub.com", "xvideos.com", "xnxx.com", "xhamster.com", 136→ "redtube.com", "youporn.com", "onlyfans.com", "chaturbate.com" 137→ ]) 138→ adultKeywords = ["porn", "xxx", "sex", "adult", "nsfw", "nude"] 139→ } 140→ 141→ private func loadDefaultDrugDomains() { 142→ drugDomains = Set([ 143→ "erowid.org", "bluelight.org", "shroomery.org" 144→ ]) 145→ drugKeywords = ["cocaine", "cocaina", "meth", "heroin", "fentanyl", "darknet market"] 146→ } 147→ 148→ private func loadDefaultGamblingDomains() { 149→ gamblingDomains = Set([ 150→ "bet365.com", "stake.com", "roobet.com", "blaze.com" 151→ ]) 152→ gamblingKeywords = ["casino", "slots", "betting", "apostas"] 153→ } 154→ 155→ private func loadDefaultRiskyApps() { 156→ riskyAppBundleIds = Set([ 157→ "com.nordvpn.osx", "com.expressvpn.ExpressVPN", 158→ "org.torproject.torbrowser" 159→ ]) 160→ riskyAppKeywords = ["vpn", "proxy", "tunnel", "tor", "anonymous"] 161→ } 162→ 163→ // MARK: - Checking Methods 164→ 165→ /// Check if a domain is blocked and return the category 166→ func checkDomain(_ domain: String) -> BlockResult { 167→ let domainLower = domain.lowercased() 168→ 169→ // Check exact matches first 170→ if adultDomains.contains(domainLower) || adultDomains.contains(where: { domainLower.contains($0) }) { 171→ return BlockResult(isBlocked: true, category: .adult, reason: "Adult content domain") 172→ } 173→ 174→ if drugDomains.contains(domainLower) || drugDomains.contains(where: { domainLower.contains($0) }) { 175→ return BlockResult(isBlocked: true, category: .drug, reason: "Drug-related domain") 176→ } 177→ 178→ if gamblingDomains.contains(domainLower) || gamblingDomains.contains(where: { domainLower.contains($0) }) { 179→ return BlockResult(isBlocked: true, category: .gambling, reason: "Gambling domain") 180→ } 181→ 182→ // Check Reddit subreddits 183→ if domainLower.contains("reddit.com") { 184→ for subreddit in drugSubreddits { 185→ if domainLower.contains("/r/\(subreddit)") { 186→ return BlockResult(isBlocked: true, category: .drug, reason: "Blocked subreddit: r/\(subreddit)") 187→ } 188→ } 189→ } 190→ 191→ // Check keywords in domain 192→ for keyword in adultKeywords { 193→ if domainLower.contains(keyword) { 194→ return BlockResult(isBlocked: true, category: .adult, reason: "Domain contains adult keyword: \(keyword)") 195→ } 196→ } 197→ 198→ for keyword in drugKeywords { 199→ if domainLower.contains(keyword) { 200→ return BlockResult(isBlocked: true, category: .drug, reason: "Domain contains drug keyword: \(keyword)") 201→ } 202→ } 203→ 204→ for keyword in gamblingKeywords { 205→ if domainLower.contains(keyword) { 206→ return BlockResult(isBlocked: true, category: .gambling, reason: "Domain contains gambling keyword: \(keyword)") 207→ } 208→ } 209→ 210→ return BlockResult(isBlocked: false, category: nil, reason: nil) 211→ } 212→ 213→ /// Check if an app is risky 214→ func checkApp(bundleId: String, appName: String) -> AppRiskResult { 215→ let bundleLower = bundleId.lowercased() 216→ let nameLower = appName.lowercased() 217→ 218→ // Check bundle ID 219→ if riskyAppBundleIds.contains(bundleLower) { 220→ return AppRiskResult(isRisky: true, riskLevel: .high, reason: "Known risky application") 221→ } 222→ 223→ // Check app name keywords 224→ for keyword in riskyAppKeywords { 225→ if nameLower.contains(keyword) { 226→ return AppRiskResult(isRisky: true, riskLevel: .moderate, reason: "App name contains risky keyword: \(keyword)") 227→ } 228→ } 229→ 230→ return AppRiskResult(isRisky: false, riskLevel: .none, reason: nil) 231→ } 232→ 233→ /// Check a full URL (domain + path) 234→ func checkURL(_ urlString: String) -> BlockResult { 235→ guard let url = URL(string: urlString), 236→ let host = url.host else { 237→ return BlockResult(isBlocked: false, category: nil, reason: nil) 238→ } 239→ 240→ // Check domain 241→ let result = checkDomain(host) 242→ if result.isBlocked { 243→ return result 244→ } 245→ 246→ // Check full path for keywords 247→ let fullPath = urlString.lowercased() 248→ 249→ for keyword in adultKeywords { 250→ if fullPath.contains(keyword) { 251→ return BlockResult(isBlocked: true, category: .adult, reason: "URL contains adult keyword: \(keyword)") 252→ } 253→ } 254→ 255→ for keyword in drugKeywords { 256→ if fullPath.contains(keyword) { 257→ return BlockResult(isBlocked: true, category: .drug, reason: "URL contains drug keyword: \(keyword)") 258→ } 259→ } 260→ 261→ return BlockResult(isBlocked: false, category: nil, reason: nil) 262→ } 263→ 264→ // MARK: - Statistics 265→ 266→ func getStats() -> BlocklistStats { 267→ return BlocklistStats( 268→ adultDomainsCount: adultDomains.count, 269→ drugDomainsCount: drugDomains.count, 270→ gamblingDomainsCount: gamblingDomains.count, 271→ riskyAppsCount: riskyAppBundleIds.count, 272→ totalKeywords: adultKeywords.count + drugKeywords.count + gamblingKeywords.count + riskyAppKeywords.count 273→ ) 274→ } 275→} 276→ 277→// MARK: - Supporting Types 278→ 279→struct BlockResult { 280→ let isBlocked: Bool 281→ let category: BlockCategory? 282→ let reason: String? 283→} 284→ 285→enum BlockCategory: String { 286→ case adult = "adult" 287→ case drug = "drug" 288→ case gambling = "gambling" 289→ 290→ var displayName: String { 291→ switch self { 292→ case .adult: return "Conteúdo Adulto" 293→ case .drug: return "Drogas" 294→ case .gambling: return "Apostas" 295→ } 296→ } 297→ 298→ var icon: String { 299→ switch self { 300→ case .adult: return "eye.slash.fill" 301→ case .drug: return "pills.fill" 302→ case .gambling: return "dice.fill" 303→ } 304→ } 305→} 306→ 307→struct AppRiskResult { 308→ let isRisky: Bool 309→ let riskLevel: AppRiskLevel 310→ let reason: String? 311→ 312→ enum AppRiskLevel: Int { 313→ case none = 0 314→ case low = 1 315→ case moderate = 2 316→ case high = 3 317→ case critical = 4 318→ } 319→} 320→ 321→struct BlocklistStats { 322→ let adultDomainsCount: Int 323→ let drugDomainsCount: Int 324→ let gamblingDomainsCount: Int 325→ let riskyAppsCount: Int 326→ let totalKeywords: Int 327→ 328→ var totalDomains: Int { 329→ adultDomainsCount + drugDomainsCount + gamblingDomainsCount 330→ } 331→} 332→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.