|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2017 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a cache for Google Safe Browsing. |
|
8 """ |
|
9 |
|
10 # |
|
11 # Some part of this code were ported from gglsbl.storage and adapted |
|
12 # to QtSql. |
|
13 # |
|
14 # https://github.com/afilipovich/gglsbl |
|
15 # |
|
16 |
|
17 from __future__ import unicode_literals |
|
18 |
|
19 import os |
|
20 |
|
21 from PyQt5.QtCore import QObject |
|
22 from PyQt5.QtSql import QSqlDatabase, QSqlQuery |
|
23 |
|
24 |
|
25 class ThreatList(object): |
|
26 """ |
|
27 Class implementing the threat list info. |
|
28 """ |
|
29 def __init__(self, threatType, platformType, threatEntryType): |
|
30 """ |
|
31 Constructor |
|
32 |
|
33 @param threatType threat type |
|
34 @type str |
|
35 @param platformType platform type |
|
36 @type str |
|
37 @param threatEntryType threat entry type |
|
38 @type str |
|
39 """ |
|
40 self.__threatType = threatType |
|
41 self.__platformType = platformType |
|
42 self.__threatEntryType = threatEntryType |
|
43 |
|
44 @classmethod |
|
45 def fromApiEntry(cls, entry): |
|
46 """ |
|
47 Class method to instantiate a threat list given a threat list entry |
|
48 dictionary. |
|
49 |
|
50 @param entry threat list entry dictionary |
|
51 @type dict |
|
52 @return instantiated object |
|
53 @rtype ThreatList |
|
54 """ |
|
55 return cls(entry['threatType'], entry['platformType'], |
|
56 entry['threatEntryType']) |
|
57 |
|
58 def asTuple(self): |
|
59 """ |
|
60 Public method to convert the object to a tuple. |
|
61 |
|
62 @return tuple containing the threat list info |
|
63 @rtype tuple of (str, str, str) |
|
64 """ |
|
65 return (self.__threatType, self.platformType, self.__threatEntryType) |
|
66 |
|
67 def __repr__(self): |
|
68 """ |
|
69 Special method to generate a printable representation. |
|
70 |
|
71 @return printable representation |
|
72 @rtype str |
|
73 """ |
|
74 return '/'.join(self.asTuple()) |
|
75 |
|
76 |
|
77 class SafeBrowsingCache(QObject): |
|
78 """ |
|
79 Class implementing a cache for Google Safe Browsing. |
|
80 """ |
|
81 |
|
82 create_threat_list_stmt = """ |
|
83 CREATE TABLE threat_list |
|
84 (threat_type character varying(128) NOT NULL, |
|
85 platform_type character varying(128) NOT NULL, |
|
86 threat_entry_type character varying(128) NOT NULL, |
|
87 client_state character varying(42), |
|
88 timestamp timestamp without time zone DEFAULT current_timestamp, |
|
89 PRIMARY KEY (threat_type, platform_type, threat_entry_type) |
|
90 ) |
|
91 """ |
|
92 drop_threat_list_stmt = """DROP TABLE IF EXISTS threat_list""" |
|
93 |
|
94 create_full_hashes_stmt = """ |
|
95 CREATE TABLE full_hash |
|
96 (value BLOB NOT NULL, |
|
97 threat_type character varying(128) NOT NULL, |
|
98 platform_type character varying(128) NOT NULL, |
|
99 threat_entry_type character varying(128) NOT NULL, |
|
100 downloaded_at timestamp without time zone DEFAULT current_timestamp, |
|
101 expires_at timestamp without time zone |
|
102 NOT NULL DEFAULT current_timestamp, |
|
103 malware_threat_type varchar(32), |
|
104 PRIMARY KEY (value, threat_type, platform_type, threat_entry_type) |
|
105 ) |
|
106 """ |
|
107 drop_full_hashes_stmt = """DROP TABLE IF EXISTS full_hash""" |
|
108 |
|
109 create_hash_prefix_stmt = """ |
|
110 CREATE TABLE hash_prefix |
|
111 (value BLOB NOT NULL, |
|
112 cue character varying(4) NOT NULL, |
|
113 threat_type character varying(128) NOT NULL, |
|
114 platform_type character varying(128) NOT NULL, |
|
115 threat_entry_type character varying(128) NOT NULL, |
|
116 timestamp timestamp without time zone DEFAULT current_timestamp, |
|
117 negative_expires_at timestamp without time zone |
|
118 NOT NULL DEFAULT current_timestamp, |
|
119 PRIMARY KEY (value, threat_type, platform_type, threat_entry_type), |
|
120 FOREIGN KEY(threat_type, platform_type, threat_entry_type) |
|
121 REFERENCES threat_list(threat_type, platform_type, threat_entry_type) |
|
122 ON DELETE CASCADE |
|
123 ) |
|
124 """ |
|
125 drop_hash_prefix_stmt = """DROP TABLE IF EXISTS hash_prefix""" |
|
126 |
|
127 create_full_hash_cue_idx = """ |
|
128 CREATE INDEX idx_hash_prefix_cue ON hash_prefix (cue) |
|
129 """ |
|
130 drop_full_hash_cue_idx = """DROP INDEX IF EXISTS idx_hash_prefix_cue""" |
|
131 |
|
132 create_full_hash_expires_idx = """ |
|
133 CREATE INDEX idx_full_hash_expires_at ON full_hash (expires_at) |
|
134 """ |
|
135 drop_full_hash_expires_idx = """ |
|
136 DROP INDEX IF EXISTS idx_full_hash_expires_at |
|
137 """ |
|
138 |
|
139 create_full_hash_value_idx = """ |
|
140 CREATE INDEX idx_full_hash_value ON full_hash (value) |
|
141 """ |
|
142 drop_full_hash_value_idx = """DROP INDEX IF EXISTS idx_full_hash_value""" |
|
143 |
|
144 def __init__(self, dbPath, parent=None): |
|
145 """ |
|
146 Constructor |
|
147 |
|
148 @param dbPath path to store the cache DB into |
|
149 @type str |
|
150 @param parent reference to the parent object |
|
151 @type QObject |
|
152 """ |
|
153 super(SafeBrowsingCache, self).__init__(parent) |
|
154 |
|
155 self.__connectionName = "SafeBrowsingCache" |
|
156 |
|
157 if not os.path.exists(dbPath): |
|
158 os.makedirs(dbPath) |
|
159 |
|
160 self.__dbFileName = os.path.join(dbPath, "SafeBrowsingCache.db") |
|
161 preparationNeeded = not os.path.exists(self.__dbFileName) |
|
162 |
|
163 self.__openCacheDb() |
|
164 if preparationNeeded: |
|
165 self.__prepareCacheDb() |
|
166 |
|
167 def __openCacheDb(self): |
|
168 """ |
|
169 Private method to open the cache database. |
|
170 |
|
171 @return flag indicating the open state |
|
172 @rtype bool |
|
173 """ |
|
174 db = QSqlDatabase.database(self.__connectionName, False) |
|
175 if not db.isValid(): |
|
176 # the database connection is a new one |
|
177 db = QSqlDatabase.addDatabase("QSQLITE", self.__connectionName) |
|
178 db.setDatabaseName(self.__dbFileName) |
|
179 opened = db.open() |
|
180 if not opened: |
|
181 QSqlDatabase.removeDatabase(self.__connectionName) |
|
182 else: |
|
183 opened = True |
|
184 return opened |
|
185 |
|
186 def __prepareCacheDb(self): |
|
187 """ |
|
188 Private method to prepare the cache database. |
|
189 """ |
|
190 db = QSqlDatabase.database(self.__connectionName) |
|
191 db.transaction() |
|
192 try: |
|
193 query = QSqlQuery(db) |
|
194 # step 1: drop old tables |
|
195 query.exec_(self.drop_threat_list_stmt) |
|
196 query.exec_(self.drop_full_hashes_stmt) |
|
197 query.exec_(self.drop_hash_prefix_stmt) |
|
198 # step 2: drop old indices |
|
199 query.exec_(self.drop_full_hash_cue_idx) |
|
200 query.exec_(self.drop_full_hash_expires_idx) |
|
201 query.exec_(self.drop_full_hash_value_idx) |
|
202 # step 3: create tables |
|
203 query.exec_(self.create_threat_list_stmt) |
|
204 query.exec_(self.create_full_hashes_stmt) |
|
205 query.exec_(self.create_hash_prefix_stmt) |
|
206 # step 4: create indices |
|
207 query.exec_(self.create_full_hash_cue_idx) |
|
208 query.exec_(self.create_full_hash_expires_idx) |
|
209 query.exec_(self.create_full_hash_value_idx) |
|
210 finally: |
|
211 del query |
|
212 db.commit() |
|
213 |
|
214 def lookupFullHashes(self, hashValues): |
|
215 """ |
|
216 Public method to get a list of threat lists and expiration flag |
|
217 for the given hashes if a hash is blacklisted. |
|
218 |
|
219 @param hashValues list of hash values to look up |
|
220 @type list of bytes |
|
221 @return list of tuples containing the threat list info and the |
|
222 expiration flag |
|
223 @rtype list of tuple of (ThreatList, bool) |
|
224 """ |
|
225 queryStr = """ |
|
226 SELECT threat_type, platform_type, threat_entry_type, |
|
227 expires_at < current_timestamp AS has_expired |
|
228 FROM full_hash WHERE value IN ({}) |
|
229 """ |
|
230 output = [] |
|
231 |
|
232 db = QSqlDatabase.database(self.__connectionName) |
|
233 if db.isOpen(): |
|
234 db.transaction() |
|
235 try: |
|
236 query = QSqlQuery(db) |
|
237 query.prepare( |
|
238 queryStr.format(",".join(["?" * len(hashValues)]))) |
|
239 for hashValue in hashValues: |
|
240 query.addBindValue(hashValue) |
|
241 |
|
242 query.exec_() |
|
243 |
|
244 while query.next(): |
|
245 threatType = query.value(0) |
|
246 platformType = query.value(1) |
|
247 threatEntryType = query.value(2) |
|
248 hasExpired = query.value(3) # TODO: check if bool |
|
249 threatList = ThreatList(threatType, platformType, |
|
250 threatEntryType) |
|
251 output.append((threatList, hasExpired)) |
|
252 del query |
|
253 finally: |
|
254 db.commit() |
|
255 |
|
256 return output |
|
257 |
|
258 def lookupHashPrefix(self, prefixes): |
|
259 """ |
|
260 Public method to look up hash prefixes in the local cache. |
|
261 |
|
262 @param prefix hash prefix to look up |
|
263 @type list of bytes |
|
264 @return list of tuples containing the threat list, full hash and |
|
265 negative cache expiration flag |
|
266 @rtype list of tuple of (ThreatList, bytes, bool) |
|
267 """ |
|
268 queryStr = """ |
|
269 SELECT value,threat_type,platform_type,threat_entry_type, |
|
270 negative_expires_at < current_timestamp AS negative_cache_expired |
|
271 FROM hash_prefix WHERE cue IN ({}) |
|
272 """ |
|
273 output = [] |
|
274 |
|
275 db = QSqlDatabase.database(self.__connectionName) |
|
276 if db.isOpen(): |
|
277 db.transaction() |
|
278 try: |
|
279 query = QSqlQuery(db) |
|
280 query.prepare( |
|
281 queryStr.format(",".join(["?" * len(prefixes)]))) |
|
282 for prefix in prefixes: |
|
283 query.addBindValue(prefix) |
|
284 |
|
285 query.exec_() |
|
286 |
|
287 while query.next(): |
|
288 fullHash = bytes(query.value(0)) |
|
289 threatType = query.value(1) |
|
290 platformType = query.value(2) |
|
291 threatEntryType = query.value(3) |
|
292 negativeCacheExpired = query.value(4) |
|
293 threatList = ThreatList(threatType, platformType, |
|
294 threatEntryType) |
|
295 output.append((threatList, fullHash, negativeCacheExpired)) |
|
296 del query |
|
297 finally: |
|
298 db.commit() |
|
299 |
|
300 return output |