eric7/WebBrowser/SafeBrowsing/SafeBrowsingCache.py

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

eric ide

mercurial