eric6/WebBrowser/SafeBrowsing/SafeBrowsingCache.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7022
a5f9f601b6ac
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2017 - 2019 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, division
18
19 import os
20
21 from PyQt5.QtCore import QObject, QByteArray, QCryptographicHash, \
22 QCoreApplication, QEventLoop
23 from PyQt5.QtSql import QSql, QSqlDatabase, QSqlQuery
24
25 from .SafeBrowsingUtilities import toHex
26 from .SafeBrowsingThreatList import ThreatList
27
28
29 class SafeBrowsingCache(QObject):
30 """
31 Class implementing a cache for Google Safe Browsing.
32 """
33 create_threat_list_stmt = """
34 CREATE TABLE threat_list
35 (threat_type character varying(128) NOT NULL,
36 platform_type character varying(128) NOT NULL,
37 threat_entry_type character varying(128) NOT NULL,
38 client_state character varying(42),
39 timestamp timestamp without time zone DEFAULT current_timestamp,
40 PRIMARY KEY (threat_type, platform_type, threat_entry_type)
41 )
42 """
43 drop_threat_list_stmt = """DROP TABLE IF EXISTS threat_list"""
44
45 create_full_hashes_stmt = """
46 CREATE TABLE full_hash
47 (value BLOB NOT NULL,
48 threat_type character varying(128) NOT NULL,
49 platform_type character varying(128) NOT NULL,
50 threat_entry_type character varying(128) NOT NULL,
51 downloaded_at timestamp without time zone DEFAULT current_timestamp,
52 expires_at timestamp without time zone
53 NOT NULL DEFAULT current_timestamp,
54 malware_threat_type varchar(32),
55 PRIMARY KEY (value, threat_type, platform_type, threat_entry_type)
56 )
57 """
58 drop_full_hashes_stmt = """DROP TABLE IF EXISTS full_hash"""
59
60 create_hash_prefix_stmt = """
61 CREATE TABLE hash_prefix
62 (value BLOB NOT NULL,
63 cue character varying(4) NOT NULL,
64 threat_type character varying(128) NOT NULL,
65 platform_type character varying(128) NOT NULL,
66 threat_entry_type character varying(128) NOT NULL,
67 timestamp timestamp without time zone DEFAULT current_timestamp,
68 negative_expires_at timestamp without time zone
69 NOT NULL DEFAULT current_timestamp,
70 PRIMARY KEY (value, threat_type, platform_type, threat_entry_type),
71 FOREIGN KEY(threat_type, platform_type, threat_entry_type)
72 REFERENCES threat_list(threat_type, platform_type, threat_entry_type)
73 ON DELETE CASCADE
74 )
75 """
76 drop_hash_prefix_stmt = """DROP TABLE IF EXISTS hash_prefix"""
77
78 create_full_hash_cue_idx = """
79 CREATE INDEX idx_hash_prefix_cue ON hash_prefix (cue)
80 """
81 drop_full_hash_cue_idx = """DROP INDEX IF EXISTS idx_hash_prefix_cue"""
82
83 create_full_hash_expires_idx = """
84 CREATE INDEX idx_full_hash_expires_at ON full_hash (expires_at)
85 """
86 drop_full_hash_expires_idx = """
87 DROP INDEX IF EXISTS idx_full_hash_expires_at
88 """
89
90 create_full_hash_value_idx = """
91 CREATE INDEX idx_full_hash_value ON full_hash (value)
92 """
93 drop_full_hash_value_idx = """DROP INDEX IF EXISTS idx_full_hash_value"""
94
95 maxProcessEventsTime = 500
96
97 def __init__(self, dbPath, parent=None):
98 """
99 Constructor
100
101 @param dbPath path to store the cache DB into
102 @type str
103 @param parent reference to the parent object
104 @type QObject
105 """
106 super(SafeBrowsingCache, self).__init__(parent)
107
108 self.__connectionName = "SafeBrowsingCache"
109
110 if not os.path.exists(dbPath):
111 os.makedirs(dbPath)
112
113 self.__dbFileName = os.path.join(dbPath, "SafeBrowsingCache.db")
114 preparationNeeded = not os.path.exists(self.__dbFileName)
115
116 self.__openCacheDb()
117 if preparationNeeded:
118 self.prepareCacheDb()
119
120 def close(self):
121 """
122 Public method to close the database.
123 """
124 if QSqlDatabase.database(self.__connectionName).isOpen():
125 QSqlDatabase.database(self.__connectionName).close()
126 QSqlDatabase.removeDatabase(self.__connectionName)
127
128 def __openCacheDb(self):
129 """
130 Private method to open the cache database.
131
132 @return flag indicating the open state
133 @rtype bool
134 """
135 db = QSqlDatabase.database(self.__connectionName, False)
136 if not db.isValid():
137 # the database connection is a new one
138 db = QSqlDatabase.addDatabase("QSQLITE", self.__connectionName)
139 db.setDatabaseName(self.__dbFileName)
140 opened = db.open()
141 if not opened:
142 QSqlDatabase.removeDatabase(self.__connectionName)
143 else:
144 opened = True
145 return opened
146
147 def prepareCacheDb(self):
148 """
149 Public method to prepare the cache database.
150 """
151 db = QSqlDatabase.database(self.__connectionName)
152 db.transaction()
153 try:
154 query = QSqlQuery(db)
155 # step 1: drop old tables
156 query.exec_(self.drop_threat_list_stmt)
157 query.exec_(self.drop_full_hashes_stmt)
158 query.exec_(self.drop_hash_prefix_stmt)
159 # step 2: drop old indices
160 query.exec_(self.drop_full_hash_cue_idx)
161 query.exec_(self.drop_full_hash_expires_idx)
162 query.exec_(self.drop_full_hash_value_idx)
163 # step 3: create tables
164 query.exec_(self.create_threat_list_stmt)
165 query.exec_(self.create_full_hashes_stmt)
166 query.exec_(self.create_hash_prefix_stmt)
167 # step 4: create indices
168 query.exec_(self.create_full_hash_cue_idx)
169 query.exec_(self.create_full_hash_expires_idx)
170 query.exec_(self.create_full_hash_value_idx)
171 finally:
172 del query
173 db.commit()
174
175 def lookupFullHashes(self, hashValues):
176 """
177 Public method to get a list of threat lists and expiration flag
178 for the given hashes if a hash is blacklisted.
179
180 @param hashValues list of hash values to look up
181 @type list of bytes
182 @return list of tuples containing the threat list info and the
183 expiration flag
184 @rtype list of tuple of (ThreatList, bool)
185 """
186 queryStr = """
187 SELECT threat_type, platform_type, threat_entry_type,
188 expires_at < current_timestamp AS has_expired
189 FROM full_hash WHERE value IN ({0})
190 """
191 output = []
192
193 db = QSqlDatabase.database(self.__connectionName)
194 if db.isOpen():
195 db.transaction()
196 try:
197 query = QSqlQuery(db)
198 query.prepare(
199 queryStr.format(",".join(["?"] * len(hashValues))))
200 for hashValue in hashValues:
201 query.addBindValue(QByteArray(hashValue),
202 QSql.In | QSql.Binary)
203
204 query.exec_()
205
206 while query.next(): # __IGNORE_WARNING_M513__
207 threatType = query.value(0)
208 platformType = query.value(1)
209 threatEntryType = query.value(2)
210 hasExpired = bool(query.value(3))
211 threatList = ThreatList(threatType, platformType,
212 threatEntryType)
213 output.append((threatList, hasExpired))
214 QCoreApplication.processEvents(QEventLoop.AllEvents,
215 self.maxProcessEventsTime)
216 del query
217 finally:
218 db.commit()
219
220 return output
221
222 def lookupHashPrefix(self, prefixes):
223 """
224 Public method to look up hash prefixes in the local cache.
225
226 @param prefixes list of hash prefixes to look up
227 @type list of bytes
228 @return list of tuples containing the threat list, full hash and
229 negative cache expiration flag
230 @rtype list of tuple of (ThreatList, bytes, bool)
231 """
232 queryStr = """
233 SELECT value,threat_type,platform_type,threat_entry_type,
234 negative_expires_at < current_timestamp AS negative_cache_expired
235 FROM hash_prefix WHERE cue IN ({0})
236 """
237 output = []
238
239 db = QSqlDatabase.database(self.__connectionName)
240 if db.isOpen():
241 db.transaction()
242 try:
243 query = QSqlQuery(db)
244 query.prepare(
245 queryStr.format(",".join(["?"] * len(prefixes))))
246 for prefix in prefixes:
247 query.addBindValue(prefix)
248
249 query.exec_()
250
251 while query.next(): # __IGNORE_WARNING_M513__
252 fullHash = bytes(query.value(0))
253 threatType = query.value(1)
254 platformType = query.value(2)
255 threatEntryType = query.value(3)
256 negativeCacheExpired = bool(query.value(4))
257 threatList = ThreatList(threatType, platformType,
258 threatEntryType)
259 output.append((threatList, fullHash, negativeCacheExpired))
260 QCoreApplication.processEvents(QEventLoop.AllEvents,
261 self.maxProcessEventsTime)
262 del query
263 finally:
264 db.commit()
265
266 return output
267
268 def storeFullHash(self, threatList, hashValue, cacheDuration,
269 malwareThreatType):
270 """
271 Public method to store full hash data in the cache database.
272
273 @param threatList threat list info object
274 @type ThreatList
275 @param hashValue hash to be stored
276 @type bytes
277 @param cacheDuration duration the data should remain in the cache
278 @type int or float
279 @param malwareThreatType threat type of the malware
280 @type str
281 """
282 insertQueryStr = """
283 INSERT OR IGNORE INTO full_hash
284 (value, threat_type, platform_type, threat_entry_type,
285 malware_threat_type, downloaded_at)
286 VALUES
287 (?, ?, ?, ?, ?, current_timestamp)
288 """
289 updateQueryStr = """
290 UPDATE full_hash SET
291 expires_at=datetime(current_timestamp, '+{0} SECONDS')
292 WHERE value=? AND threat_type=? AND platform_type=? AND
293 threat_entry_type=?
294 """.format(int(cacheDuration))
295
296 db = QSqlDatabase.database(self.__connectionName)
297 if db.isOpen():
298 db.transaction()
299 try:
300 query = QSqlQuery(db)
301 query.prepare(insertQueryStr)
302 query.addBindValue(QByteArray(hashValue),
303 QSql.In | QSql.Binary)
304 query.addBindValue(threatList.threatType)
305 query.addBindValue(threatList.platformType)
306 query.addBindValue(threatList.threatEntryType)
307 query.addBindValue(malwareThreatType)
308 query.exec_()
309 del query
310
311 query = QSqlQuery(db)
312 query.prepare(updateQueryStr)
313 query.addBindValue(QByteArray(hashValue),
314 QSql.In | QSql.Binary)
315 query.addBindValue(threatList.threatType)
316 query.addBindValue(threatList.platformType)
317 query.addBindValue(threatList.threatEntryType)
318 query.exec_()
319 del query
320 finally:
321 db.commit()
322
323 def deleteHashPrefixList(self, threatList):
324 """
325 Public method to delete hash prefixes for a given threat list.
326
327 @param threatList threat list info object
328 @type ThreatList
329 """
330 queryStr = """
331 DELETE FROM hash_prefix
332 WHERE threat_type=? AND platform_type=? AND threat_entry_type=?
333 """
334
335 db = QSqlDatabase.database(self.__connectionName)
336 if db.isOpen():
337 db.transaction()
338 try:
339 query = QSqlQuery(db)
340 query.prepare(queryStr)
341 query.addBindValue(threatList.threatType)
342 query.addBindValue(threatList.platformType)
343 query.addBindValue(threatList.threatEntryType)
344 query.exec_()
345 del query
346 finally:
347 db.commit()
348
349 def cleanupFullHashes(self, keepExpiredFor=43200):
350 """
351 Public method to clean up full hash entries expired more than the
352 given time.
353
354 @param keepExpiredFor time period in seconds of entries to be expired
355 @type int or float
356 """
357 queryStr = """
358 DELETE FROM full_hash
359 WHERE expires_at=datetime(current_timestamp, '{0} SECONDS')
360 """.format(int(keepExpiredFor))
361
362 db = QSqlDatabase.database(self.__connectionName)
363 if db.isOpen():
364 db.transaction()
365 try:
366 query = QSqlQuery(db)
367 query.prepare(queryStr)
368 query.exec_()
369 del query
370 finally:
371 db.commit()
372
373 def updateHashPrefixExpiration(self, threatList, hashPrefix,
374 negativeCacheDuration):
375 """
376 Public method to update the hash prefix expiration time.
377
378 @param threatList threat list info object
379 @type ThreatList
380 @param hashPrefix hash prefix
381 @type bytes
382 @param negativeCacheDuration time in seconds the entry should remain
383 in the cache
384 @type int or float
385 """
386 queryStr = """
387 UPDATE hash_prefix
388 SET negative_expires_at=datetime(current_timestamp, '+{0} SECONDS')
389 WHERE value=? AND threat_type=? AND platform_type=? AND
390 threat_entry_type=?
391 """.format(int(negativeCacheDuration))
392
393 db = QSqlDatabase.database(self.__connectionName)
394 if db.isOpen():
395 db.transaction()
396 try:
397 query = QSqlQuery(db)
398 query.prepare(queryStr)
399 query.addBindValue(QByteArray(hashPrefix),
400 QSql.In | QSql.Binary)
401 query.addBindValue(threatList.threatType)
402 query.addBindValue(threatList.platformType)
403 query.addBindValue(threatList.threatEntryType)
404 query.exec_()
405 del query
406 finally:
407 db.commit()
408
409 def getThreatLists(self):
410 """
411 Public method to get the available threat lists.
412
413 @return list of available threat lists
414 @rtype list of tuples of (ThreatList, str)
415 """
416 queryStr = """
417 SELECT threat_type,platform_type,threat_entry_type,client_state
418 FROM threat_list
419 """
420 output = []
421
422 db = QSqlDatabase.database(self.__connectionName)
423 if db.isOpen():
424 db.transaction()
425 try:
426 query = QSqlQuery(db)
427 query.prepare(queryStr)
428
429 query.exec_()
430
431 while query.next(): # __IGNORE_WARNING_M513__
432 threatType = query.value(0)
433 platformType = query.value(1)
434 threatEntryType = query.value(2)
435 clientState = query.value(3)
436 threatList = ThreatList(threatType, platformType,
437 threatEntryType)
438 output.append((threatList, clientState))
439 QCoreApplication.processEvents(QEventLoop.AllEvents,
440 self.maxProcessEventsTime)
441 del query
442 finally:
443 db.commit()
444
445 return output
446
447 def addThreatList(self, threatList):
448 """
449 Public method to add a threat list to the cache.
450
451 @param threatList threat list to be added
452 @type ThreatList
453 """
454 queryStr = """
455 INSERT OR IGNORE INTO threat_list
456 (threat_type, platform_type, threat_entry_type, timestamp)
457 VALUES (?, ?, ?, current_timestamp)
458 """
459
460 db = QSqlDatabase.database(self.__connectionName)
461 if db.isOpen():
462 db.transaction()
463 try:
464 query = QSqlQuery(db)
465 query.prepare(queryStr)
466 query.addBindValue(threatList.threatType)
467 query.addBindValue(threatList.platformType)
468 query.addBindValue(threatList.threatEntryType)
469 query.exec_()
470 del query
471 finally:
472 db.commit()
473
474 def deleteThreatList(self, threatList):
475 """
476 Public method to delete a threat list from the cache.
477
478 @param threatList threat list to be deleted
479 @type ThreatList
480 """
481 queryStr = """
482 DELETE FROM threat_list
483 WHERE threat_type=? AND platform_type=? AND threat_entry_type=?
484 """
485
486 db = QSqlDatabase.database(self.__connectionName)
487 if db.isOpen():
488 db.transaction()
489 try:
490 query = QSqlQuery(db)
491 query.prepare(queryStr)
492 query.addBindValue(threatList.threatType)
493 query.addBindValue(threatList.platformType)
494 query.addBindValue(threatList.threatEntryType)
495 query.exec_()
496 del query
497 finally:
498 db.commit()
499
500 def updateThreatListClientState(self, threatList, clientState):
501 """
502 Public method to update the client state of a threat list.
503
504 @param threatList threat list to update the client state for
505 @type ThreatList
506 @param clientState new client state
507 @type str
508 """
509 queryStr = """
510 UPDATE threat_list SET timestamp=current_timestamp, client_state=?
511 WHERE threat_type=? AND platform_type=? AND threat_entry_type=?
512 """
513
514 db = QSqlDatabase.database(self.__connectionName)
515 if db.isOpen():
516 db.transaction()
517 try:
518 query = QSqlQuery(db)
519 query.prepare(queryStr)
520 query.addBindValue(clientState)
521 query.addBindValue(threatList.threatType)
522 query.addBindValue(threatList.platformType)
523 query.addBindValue(threatList.threatEntryType)
524 query.exec_()
525 del query
526 finally:
527 db.commit()
528
529 def hashPrefixListChecksum(self, threatList):
530 """
531 Public method to calculate the SHA256 checksum for an alphabetically
532 sorted concatenated list of hash prefixes.
533
534 @param threatList threat list to calculate checksum for
535 @type ThreatList
536 @return SHA256 checksum
537 @rtype bytes
538 """
539 queryStr = """
540 SELECT value FROM hash_prefix
541 WHERE threat_type=? AND platform_type=? AND threat_entry_type=?
542 ORDER BY value
543 """
544 checksum = None
545
546 db = QSqlDatabase.database(self.__connectionName)
547 if db.isOpen():
548 db.transaction()
549 sha256Hash = QCryptographicHash(QCryptographicHash.Sha256)
550 try:
551 query = QSqlQuery(db)
552 query.prepare(queryStr)
553 query.addBindValue(threatList.threatType)
554 query.addBindValue(threatList.platformType)
555 query.addBindValue(threatList.threatEntryType)
556
557 query.exec_()
558
559 while query.next(): # __IGNORE_WARNING_M513__
560 sha256Hash.addData(query.value(0))
561 QCoreApplication.processEvents(QEventLoop.AllEvents,
562 self.maxProcessEventsTime)
563 del query
564 finally:
565 db.commit()
566
567 checksum = bytes(sha256Hash.result())
568
569 return checksum
570
571 def populateHashPrefixList(self, threatList, prefixes):
572 """
573 Public method to populate the hash prefixes for a threat list.
574
575 @param threatList threat list of the hash prefixes
576 @type ThreatList
577 @param prefixes list of hash prefixes to be inserted
578 @type HashPrefixList
579 """
580 queryStr = """
581 INSERT INTO hash_prefix
582 (value, cue, threat_type, platform_type, threat_entry_type,
583 timestamp)
584 VALUES (?, ?, ?, ?, ?, current_timestamp)
585 """
586
587 db = QSqlDatabase.database(self.__connectionName)
588 if db.isOpen():
589 db.transaction()
590 try:
591 for prefix in prefixes:
592 query = QSqlQuery(db)
593 query.prepare(queryStr)
594 query.addBindValue(QByteArray(prefix),
595 QSql.In | QSql.Binary)
596 query.addBindValue(toHex(prefix[:4]))
597 query.addBindValue(threatList.threatType)
598 query.addBindValue(threatList.platformType)
599 query.addBindValue(threatList.threatEntryType)
600 query.exec_()
601 del query
602 QCoreApplication.processEvents(QEventLoop.AllEvents,
603 self.maxProcessEventsTime)
604 finally:
605 db.commit()
606
607 def getHashPrefixValuesToRemove(self, threatList, indexes):
608 """
609 Public method to get the hash prefix values to be removed from the
610 cache.
611
612 @param threatList threat list to remove prefixes from
613 @type ThreatList
614 @param indexes list of indexes of prefixes to be removed
615 @type list of int
616 @return list of hash prefixes to be removed
617 @rtype list of bytes
618 """
619 queryStr = """
620 SELECT value FROM hash_prefix
621 WHERE threat_type=? AND platform_type=? AND threat_entry_type=?
622 ORDER BY value
623 """
624 indexes = set(indexes)
625 output = []
626
627 db = QSqlDatabase.database(self.__connectionName)
628 if db.isOpen():
629 db.transaction()
630 try:
631 query = QSqlQuery(db)
632 query.prepare(queryStr)
633 query.addBindValue(threatList.threatType)
634 query.addBindValue(threatList.platformType)
635 query.addBindValue(threatList.threatEntryType)
636
637 query.exec_()
638
639 index = 0
640 while query.next(): # __IGNORE_WARNING_M513__
641 if index in indexes:
642 prefix = bytes(query.value(0))
643 output.append(prefix)
644 index += 1
645 QCoreApplication.processEvents(QEventLoop.AllEvents,
646 self.maxProcessEventsTime)
647 del query
648 finally:
649 db.commit()
650
651 return output
652
653 def removeHashPrefixIndices(self, threatList, indexes):
654 """
655 Public method to remove hash prefixes from the cache.
656
657 @param threatList threat list to delete hash prefixes of
658 @type ThreatList
659 @param indexes list of indexes of prefixes to be removed
660 @type list of int
661 """
662 queryStr = """
663 DELETE FROM hash_prefix
664 WHERE threat_type=? AND platform_type=? AND
665 threat_entry_type=? AND value IN ({0})
666 """
667 batchSize = 40
668
669 prefixesToRemove = self.getHashPrefixValuesToRemove(
670 threatList, indexes)
671 if prefixesToRemove:
672 db = QSqlDatabase.database(self.__connectionName)
673 if db.isOpen():
674 db.transaction()
675 try:
676 for index in range(0, len(prefixesToRemove), batchSize):
677 removeBatch = \
678 prefixesToRemove[index:(index + batchSize)]
679
680 query = QSqlQuery(db)
681 query.prepare(
682 queryStr.format(",".join(["?"] * len(removeBatch)))
683 )
684 query.addBindValue(threatList.threatType)
685 query.addBindValue(threatList.platformType)
686 query.addBindValue(threatList.threatEntryType)
687 for prefix in removeBatch:
688 query.addBindValue(QByteArray(prefix),
689 QSql.In | QSql.Binary)
690 query.exec_()
691 del query
692 QCoreApplication.processEvents(
693 QEventLoop.AllEvents, self.maxProcessEventsTime)
694 finally:
695 db.commit()

eric ide

mercurial