-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgdaAttack.html
More file actions
5051 lines (4678 loc) · 241 KB
/
Copy pathgdaAttack.html
File metadata and controls
5051 lines (4678 loc) · 241 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<meta name="generator" content="pdoc 0.7.5" />
<title>gdascore.gdaAttack API documentation</title>
<meta name="description" content="" />
<link href='https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.0/normalize.min.css' rel='stylesheet'>
<link href='https://cdnjs.cloudflare.com/ajax/libs/10up-sanitize.css/8.0.0/sanitize.min.css' rel='stylesheet'>
<link href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/github.min.css" rel="stylesheet">
<style>.flex{display:flex !important}body{line-height:1.5em}#content{padding:20px}#sidebar{padding:30px;overflow:hidden}.http-server-breadcrumbs{font-size:130%;margin:0 0 15px 0}#footer{font-size:.75em;padding:5px 30px;border-top:1px solid #ddd;text-align:right}#footer p{margin:0 0 0 1em;display:inline-block}#footer p:last-child{margin-right:30px}h1,h2,h3,h4,h5{font-weight:300}h1{font-size:2.5em;line-height:1.1em}h2{font-size:1.75em;margin:1em 0 .50em 0}h3{font-size:1.4em;margin:25px 0 10px 0}h4{margin:0;font-size:105%}a{color:#058;text-decoration:none;transition:color .3s ease-in-out}a:hover{color:#e82}.title code{font-weight:bold}h2[id^="header-"]{margin-top:2em}.ident{color:#900}pre code{background:#f8f8f8;font-size:.8em;line-height:1.4em}code{background:#f2f2f1;padding:1px 4px;overflow-wrap:break-word}h1 code{background:transparent}pre{background:#f8f8f8;border:0;border-top:1px solid #ccc;border-bottom:1px solid #ccc;margin:1em 0;padding:1ex}#http-server-module-list{display:flex;flex-flow:column}#http-server-module-list div{display:flex}#http-server-module-list dt{min-width:10%}#http-server-module-list p{margin-top:0}.toc ul,#index{list-style-type:none;margin:0;padding:0}#index code{background:transparent}#index h3{border-bottom:1px solid #ddd}#index ul{padding:0}#index h4{font-weight:bold}#index h4 + ul{margin-bottom:.6em}@media (min-width:200ex){#index .two-column{column-count:2}}@media (min-width:300ex){#index .two-column{column-count:3}}dl{margin-bottom:2em}dl dl:last-child{margin-bottom:4em}dd{margin:0 0 1em 3em}#header-classes + dl > dd{margin-bottom:3em}dd dd{margin-left:2em}dd p{margin:10px 0}.name{background:#eee;font-weight:bold;font-size:.85em;padding:5px 10px;display:inline-block;min-width:40%}.name:hover{background:#e0e0e0}.name > span:first-child{white-space:nowrap}.name.class > span:nth-child(2){margin-left:.4em}.inherited{color:#999;border-left:5px solid #eee;padding-left:1em}.inheritance em{font-style:normal;font-weight:bold}.desc h2{font-weight:400;font-size:1.25em}.desc h3{font-size:1em}.desc dt code{background:inherit}.source summary,.git-link-div{color:#666;text-align:right;font-weight:400;font-size:.8em;text-transform:uppercase}.source summary > *{white-space:nowrap;cursor:pointer}.git-link{color:inherit;margin-left:1em}.source pre{max-height:500px;overflow:auto;margin:0}.source pre code{font-size:12px;overflow:visible}.hlist{list-style:none}.hlist li{display:inline}.hlist li:after{content:',\2002'}.hlist li:last-child:after{content:none}.hlist .hlist{display:inline;padding-left:1em}img{max-width:100%}.admonition{padding:.1em .5em;margin-bottom:1em}.admonition-title{font-weight:bold}.admonition.note,.admonition.info,.admonition.important{background:#aef}.admonition.todo,.admonition.versionadded,.admonition.tip,.admonition.hint{background:#dfd}.admonition.warning,.admonition.versionchanged,.admonition.deprecated{background:#fd4}.admonition.error,.admonition.danger,.admonition.caution{background:lightpink}</style>
<style media="screen and (min-width: 700px)">@media screen and (min-width:700px){#sidebar{width:30%}#content{width:70%;max-width:100ch;padding:3em 4em;border-left:1px solid #ddd}pre code{font-size:1em}.item .name{font-size:1em}main{display:flex;flex-direction:row-reverse;justify-content:flex-end}.toc ul ul,#index ul{padding-left:1.5em}.toc > ul > li{margin-top:.5em}}</style>
<style media="print">@media print{#sidebar h1{page-break-before:always}.source{display:none}}@media print{*{background:transparent !important;color:#000 !important;box-shadow:none !important;text-shadow:none !important}a[href]:after{content:" (" attr(href) ")";font-size:90%}a[href][title]:after{content:none}abbr[title]:after{content:" (" attr(title) ")"}.ir a:after,a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}@page{margin:0.5cm}p,h2,h3{orphans:3;widows:3}h1,h2,h3,h4,h5,h6{page-break-after:avoid}}</style>
<style>
</style>
</head>
<body>
<main>
<article id="content">
<header>
<h1 class="title">Module <code>gdascore.gdaAttack</code></h1>
</header>
<section id="section-intro">
<details class="source">
<summary>
<span>Expand source code</span>
</summary>
<pre><code class="python">import re
import subprocess
import coloredlogs, logging
import sqlite3
import simplejson
import psycopg2
import queue
import threading
import sys
import os
import copy
import base64
import time
import pprint
import datetime
import signal
import atexit
import random
import requests
import functools
coloredlogs.DEFAULT_FIELD_STYLES['asctime'] = {}
coloredlogs.DEFAULT_FIELD_STYLES['levelname'] = {'bold': True, 'color': 'white', 'bright': True}
coloredlogs.DEFAULT_LEVEL_STYLES['info'] = {'color': 'cyan', 'bright': True}
coloredlogs.install(
fmt="[%(levelname)s] %(message)s (%(filename)s, %(funcName)s(), line %(lineno)d, %(asctime)s)",
datefmt='%Y-%m-%d %H:%M',
level=logging.INFO,
)
# logging.basicConfig(
# format="[%(levelname)s] %(message)s (%(filename)s, %(funcName)s(), line %(lineno)d, %(asctime)s)",
# datefmt='%Y-%m-%d %H:%M',
# level=logging.INFO,
# )
# for pdoc documentation
__all__ = ["gdaAttack"]
try:
from .gdaTools import getInterpolatedValue, getDatabaseInfo
from .dupCheck import DupCheck
except ImportError:
from gdaTools import getInterpolatedValue, getDatabaseInfo
from dupCheck import DupCheck
theCacheQueue = None
theCacheThreadObject = None
flgCacheThreadStarted = False
atcObject = None
class gdaAttack:
"""Manages a GDA Attack
WARNING: this code is fragile, and can fail ungracefully, or
just hang."""
def __init__(self, params):
""" Everything gets set up with 'gdaAttack(params)'
params is a dictionary containing the following
required parameters: <br/>
`param['name']`: The name of the attack. Make it unique, because
the cache is discovered using this name. <br/>
`param['rawDb']`: The label for the DB to be used as the
raw (non-anonymized) DB. <br/>
Following are the optional parameters: <br/>
`param['criteria']`: The criteria by which the attack should
determined to succeed or fail. Must be one of 'singlingOut',
'inference', or 'linkability'. Default is 'singlingOut'. <br/>
`param['anonDb']`: The label for the DB to be used as the
anonymized DB. (Is automatically set to `param['rawDb']` if
not set.) <br/>
`param['pubDb']`: The label for the DB to be used as the
publicly known DB in linkability attacks. <br/>
`param['table']`: The table to be attacked. Must be present
if the DB has more than one table. <br/>
`param['uid']`: The uid column for the table. Must be present
if the name of the column is other than 'uid'. <br/>
`param['flushCache']`: Set to true if you want the cache of
query answers from a previous run flushed. The purpose of the
cache is to save the work from an aborted attack, which can be
substantial because attacks can have hundreds of queries. <br/>
`param['locCacheDir']`: The directory holding the cache DBs.
Default 'cacheDBs'. <br/>
`param['numRawDbThreads']`: The number of parallel queries
that can be made to the raw DB. Default 3. <br/>
`param['numAnonDbThreads']`: The number of parallel queries
that can be made to the anon DB. Default 3. <br/>
`param['numPubDbThreads']`: The number of parallel queries
that can be made to the public linkability DB. Default 3. <br/>
`param['verbose']`: Set to True for verbose output.
`param['dp_budget']`: An optional overall privacy budget for the attack. For use with uber_dp. Default 'None'. <br/>
"""
#### gda-score-code version check warning ####
process = subprocess.run([sys.executable, "-m", "pip", "list","--outdated"],stdout=subprocess.PIPE,stderr=subprocess.PIPE,universal_newlines=True)
upgradable_pkgs = process.stdout
if "gda-score-code" in upgradable_pkgs:
pkgs = upgradable_pkgs.split('\n')
potential_gdascore_pkgs = list(filter(lambda x: 'gda-score-code' in x, pkgs))
if len(potential_gdascore_pkgs) == 1:
gdascore_pkg = potential_gdascore_pkgs[0]
pkg_name, curr_ver, latest_ver, ins_type = (re.sub(r'\s+', '|', gdascore_pkg)
.split('|'))
print('\n')
logging.warning(f'WARNING: You have {pkg_name} version {curr_ver} installed; '
f'however, version {latest_ver} is available.')
logging.warning(f'You should consider upgrading via the '
f'"pip install --upgrade {pkg_name}" command.')
print('\n')
########
########### added by frzmohammadali ##########
global theCacheQueue
global theCacheThreadObject
global flgCacheThreadStarted
global atcObject
if not theCacheQueue and not theCacheThreadObject:
theCacheQueue = queue.Queue()
theCacheThreadObject = CacheThread(theCacheQueue, self)
atcObject = self
printTitle('cache thread initialized.')
self.cacheQueue = theCacheQueue
self.cacheThreadObject = theCacheThreadObject
if not flgCacheThreadStarted:
self.cacheThreadObject.start()
flgCacheThreadStarted = True
##############################################
############## parameters and instance variables ###############
# ------------- Class called parameters and configured parameters
self._vb = False
self._cr = '' # short for criteria
self._pp = None # pretty printer (for debugging)
self._sid = None # for uber_dp interface, a session ID over the attack is needed
self._session = None # also session for the uber_dp interface
self._colNamesTypes = []
self._colNames = []
self._p = dict(name='',
rawDb='',
anonDb='',
pubDb='',
criteria='singlingOut',
table='',
uid='uid',
flushCache=False,
verbose=False,
# following not normally set by caller, but can be
locCacheDir="cacheDBs",
numRawDbThreads=3,
numAnonDbThreads=3,
numPubDbThreads=3,
)
self._requiredParams = ['name', 'rawDb']
# ---------- Private internal state
# Threads
self._rawThreads = []
self._anonThreads = []
self._pubThreads = []
# Queues read by database threads _rawThreads and _anonThreads
self._rawQ = None
self._anonQ = None
self._pubQ = None
# Queues read by various caller functions
self._exploreQ = None
self._knowledgeQ = None
self._attackQ = None
self._claimQ = None
self._guessQ = None
# ask/get counters for setting 'stillToCome'
self._exploreCounter = 0
self._knowledgeCounter = 0
self._attackCounter = 0
self._claimCounter = 0
self._guessCounter = 0
# State for duplicate claim detection
self._dupCheck = DupCheck()
# State for computing attack results (see _initAtkRes())
self._atrs = {}
# State for various operational measures (see _initOp())
self._op = {}
##############################################
if self._vb:
print(f"Calling {__name__}.init")
if self._vb:
print(f" {params}")
self._initOp()
self._initCounters()
self._assignGlobalParams(params)
self._doParamChecks()
for param in self._requiredParams:
if len(self._p[param]) == 0:
s = str(f"Error: Need param '{param}' in class parameters")
sys.exit(s)
# extract the type of interface we are interacting with the anonymization
self._type = self._p['anonDb']['type']
if self._type == 'uber_dp':
# cannot run attack on uber dp without specifying the budget
if self._p['dp_budget'] is None:
s = str(f"Error: Needs param dp_budget in class parameters when running uber_dp attacks")
sys.exit(s)
# Assign the privacy budget as a parameter to the attack
self._remaining_dp_budget = self._p['dp_budget']
self._initUberDPSession()
# if no session id was set, the attacks cannot be conducted
if self._sid is None:
s = str(f"Failed initializing session with Uber_DP Server")
sys.exit(s)
# create the database directory if it doesn't exist
try:
if not os.path.exists(self._p['locCacheDir']):
os.makedirs(self._p['locCacheDir'])
except OSError:
sys.exit("Error: Creating directory. " + self._p['locCacheDir'])
# Get the table name if not provided by the caller
if len(self._p['table']) == 0:
tables = self.getTableNames()
if len(tables) != 1:
print("Error: gdaAttack(): Must include table name if " +
"there is more than one table in database")
sys.exit()
self._p['table'] = tables[0]
# Get the column names for computing susceptibility later
self._colNamesTypes = self.getColNamesAndTypes()
if self._vb:
print(f"Columns are '{self._colNamesTypes}'")
self._initAtkRes()
# And make a convenient list of column names
for colNameType in self._colNamesTypes:
self._colNames.append(colNameType[0])
# Setup the database which holds already executed queries so we
# don't have to repeat them if we are restarting
self._setupLocalCacheDB()
# Setup the threads and queues
self._setupThreadsAndQueues()
numThreads = threading.active_count()
expectedThreads = (self._p['numRawDbThreads'] +
self._p['numAnonDbThreads'] + 1)
if len(self._p['pubDb']) > 0:
expectedThreads += self._p['numPubDbThreads']
if numThreads < expectedThreads:
print(f"Error: Some thread(s) died "
f"(count {numThreads}, expected {expectedThreads}). "
f"Aborting.")
self.cleanUp(cleanUpCache=False, doExit=True)
def getResults(self):
""" Returns all of the compiled attack results.
This can be input to class `gdaScores()` and method
`gdaScores.addResult()`."""
# Add the operational parameters
self._atrs['operational'] = self.getOpParameters()
self._cleanPasswords()
return self._atrs
def getOpParameters(self):
""" Returns a variety of performance measurements.
Useful for debugging."""
self._op['avQueryDuration'] = 0
if self._op['numQueries'] > 0:
self._op['avQueryDuration'] = (
self._op['timeQueries'] / self._op['numQueries'])
self._op['avCachePutDuration'] = 0
if self._op['numCachePuts'] > 0:
self._op['avCachePutDuration'] = (
self._op['timeCachePuts'] / self._op['numCachePuts'])
self._op['avCacheGetDuration'] = 0
if self._op['numCacheGets'] > 0:
self._op['avCacheGetDuration'] = (
self._op['timeCacheGets'] / self._op['numCacheGets'])
return self._op
def setVerbose(self):
"""Sets Verbose to True"""
self._vb = True
def unsetVerbose(self):
"""Sets Verbose to False"""
self._vb = False
def cleanUp(self, cleanUpCache=True, doExit=False,
exitMsg="Finished cleanUp, exiting"):
""" Garbage collect queues, threads, and cache.
By default, this wipes the cache. The idea being that if the
entire attack finished successfully, then it won't be
repeated and the cache isn't needed. Do `cleanUpCache=False`
if that isn't what you want."""
if self._vb: print(f"Calling {__name__}.cleanUp")
if self._rawQ.empty() != True:
logging.warning("Warning, trying to clean up when raw queue not empty!")
if self._anonQ.empty() != True:
logging.warning("Warning, trying to clean up when anon queue not empty!")
if self.cacheQueue.empty() != True:
logging.warning("Warning, trying to clean up when cache queue not empty!")
# Stuff in end signals for the workers (this is a bit bogus, cause
# if a thread is gone or hanging, not all signals will get read)
for i in range(self._p['numRawDbThreads']):
self._rawQ.put(None)
for i in range(self._p['numAnonDbThreads']):
self._anonQ.put(None)
for i in range(self.cacheQueue.qsize()):
self.cacheQueue.put(None)
cleanBgThreads()
if len(self._p['pubDb']) > 0:
if self._pubQ.empty() != True:
print("Warning, trying to clean up when pub queue not empty!")
for i in range(self._p['numPubDbThreads']):
self._pubQ.put(None)
for t in self._pubThreads:
if t.isAlive(): t.stop() # t.join()
if cleanUpCache:
self._removeLocalCacheDB()
if self._session: # close the uber session
self._session.close()
if doExit:
sys.exit(exitMsg)
def isClaimed(self, spec):
"""Check if a claim was already fully or partially made.
The `spec` is formatted identical to the `spec` in `gdaAttack.askClaim`."""
return self._dupCheck.is_claimed(spec, verbose=self._vb)
def askClaim(self, spec, cache=True, claim=True):
"""Generate Claim query for raw and optionally pub databases.
Before anything happens, the system uses the `gdaAttack.isClaimed`
method to determine whether a previous claim fully or partially
matches the new claim. Such duplicates are not allowed and an error
will be raised providing additional details about the duplicate.
Making a claim results in a query to the raw database, and if
linkability attack, the pub database, to check
the correctness of the claim. Multiple calls to this method will
cause the corresponding queries to be queued up, so `askClaim()`
returns immediately. `getClaim()` harvests one claim result. <br/>
Set `claim=False` if this claim should not be applied to the
confidence improvement score. In this case, the probability score
will instead be reduced accordingly. <br/>
The `spec` is formatted as follows: <br/>
{'known':[{'col':'colName','val':'value'},...],
'guess':[{'col':'colName','val':'value'},...],
}
`spec['known']` are the columns and values the attacker already knows
(i.e. with prior knowledge). Optional. <br/>
`spec['guess']` are the columns and values the attacker doesn't know,
but rather is trying to predict. Mandatory for 'singling out'
and 'inference'. Optional for 'linkabiblity' <br/>
Answers are cached <br/>
Returns immediately"""
if self._vb: print(f"Calling {__name__}.askClaim with spec '{spec}', count {self._claimCounter}")
if not self._dupCheck.is_claimed(spec, verbose=self._vb, raise_true=True):
self._dupCheck.claim(spec, verbose=self._vb)
self._claimCounter += 1
sql = self._makeSqlFromSpec(spec)
if self._vb: print(f"Sql is '{sql}'")
sqlConfs = self._makeSqlConfFromSpec(spec)
if self._vb: print(f"SqlConf is '{sqlConfs}'")
# Make a copy of the query for passing around
job = {}
job['q'] = self._claimQ
job['claim'] = claim
job['queries'] = [{'sql': sql, 'cache': cache}]
job['spec'] = spec
for sqlConf in sqlConfs:
job['queries'].append({'sql': sqlConf, 'cache': cache})
self._rawQ.put(job)
def getClaim(self):
""" Wait for and gather results of askClaim() calls
Returns a data structure that contains both the result
of one finished claim, and the claim's input parameters.
Note that the order in which results are returned by
`getClaim()` are not necessarily the same order they were
inserted by `askClaim()`. <br/>
Assuming `result` is returned: <br/>
`result['claim']` is the value supplied in the corresponding
`askClaim()` call <br/>
`result['spec']` is a copy of the `spec` supplied in the
corresponding `askClaim()` call. <br/>
`result['queries']` is a list of the queries generated in order to
validate the claim. <br/>
`result['answers']` are the answers to the queries in
`result['queries']`. <br/>
`result['claimResult']` is 'Correct' or 'Incorrect', depending
on whether the claim satisfies the critieria or not. <br/>
`result['stillToCome']` is a counter showing how many more
claims are still queued. When `stillToCome` is 0, then all
claims submitted by `askClaim()` have been returned."""
if self._vb:
print(f"Calling {__name__}.getClaim")
if self._claimCounter == 0:
# Caller shouldn't be calling if there are no expected
# answers, but is anyway, so just return
return {'query': {'sql': 'None'}, 'error': 'Nothing to do',
'stillToCome': 0, 'claimResult': 'Error'}
job = self._claimQ.get()
claim = job['claim']
self._claimQ.task_done()
self._claimCounter -= 1
job['stillToCome'] = self._claimCounter
self._addToAtkRes('claimTrials', job['spec'], 1)
# The claim is tested against the first reply
reply = job['replies'][0]
job['claimResult'] = 'Wrong'
if claim:
self._addToAtkRes('claimMade', job['spec'], 1)
if 'error' in reply:
self._addToAtkRes('claimError', job['spec'], 1)
job['claimResult'] = 'Error'
else:
if self._cr == 'singlingOut':
claimIsCorrect = self._checkSinglingOut(reply['answer'])
elif self._cr == 'inference':
claimIsCorrect = self._checkInference(reply['answer'])
elif self._cr == 'linkability':
claimIsCorrect = self._checkLinkability(reply['answer'])
if claim == 1 and claimIsCorrect:
self._addToAtkRes('claimCorrect', job['spec'], 1)
job['claimResult'] = 'Correct'
elif claim == 0 and claimIsCorrect:
self._addToAtkRes('claimPassCorrect', job['spec'], 1)
job['claimResult'] = 'Correct'
if self._cr == 'singlingOut' or self._cr == 'inference':
# Then measure confidence against the second and third replies
if 'answer' in job['replies'][1]:
if job['replies'][1]['answer']:
guessedRows = job['replies'][1]['answer'][0][0]
else:
guessedRows = 0
elif 'error' in job['replies'][1]:
self._pp.pprint(job)
print(f"Error: conf query:\n{job['replies'][1]['error']}")
self.cleanUp(cleanUpCache=False, doExit=True)
if 'answer' in job['replies'][2]:
if job['replies'][2]['answer']:
totalRows = job['replies'][2]['answer'][0][0]
else:
totalRows = 0
elif 'error' in job['replies'][2]:
self._pp.pprint(job)
print(f"Error: conf query:\n{job['replies'][2]['error']}")
self.cleanUp(cleanUpCache=False, doExit=True)
if totalRows:
self._addToAtkRes('sumConfidenceRatios', job['spec'],
guessedRows / totalRows)
self._addToAtkRes('numConfidenceRatios', job['spec'], 1)
self._atrs['tableStats']['totalRows'] = totalRows
else:
# For linkability, the confidence is always 1/2
self._addToAtkRes('sumConfidenceRatios', job['spec'], 0.5)
self._addToAtkRes('numConfidenceRatios', job['spec'], 1)
if 'q' in job:
del job['q']
return (job)
def askAttack(self, query, cache=True):
""" Generate and queue up an attack query for database.
`query` is a dictionary with (currently) one value: <br/>
`query['sql']` contains the SQL query. <br/>
`query['epsilon']` is optional, and defines how much of the differential privacy budget is used for uber_dp <br/>
"""
self._attackCounter += 1
if self._vb: print(f"Calling {__name__}.askAttack with query '{query}', count {self._attackCounter}")
# Make a copy of the query for passing around
qCopy = copy.copy(query)
job = {}
job['q'] = self._attackQ
qCopy['cache'] = cache
job['queries'] = [qCopy]
self._anonQ.put(job)
def getAttack(self):
""" Returns the result of one askAttack() call
Blocks until the result is available. Note that the order
in which results are received is not necesarily the order
in which `askAttack()` calls were made. <br/>
Assuming `result` is returned: <br/>
`result['answer']` is the answer returned by the DB. The
format is: <br/>
`[(C1,C2...,Cn),(C1,C2...,Cn), ... (C1,C2...,Cn)]` <br/>
where C1 is the first element of the `SELECT`, C2 the second
element, etc. This attribute does not exist in cases of query
error (i.e. bad sql, budget exceeded if uber_dp, etc.) <br/>
`result['cells']` is the number of cells returned in the answer
(used by `gdaAttack()` to compute total attack cells) <br/>
`result['query']['sql']` is the query from the corresponding
`askAttack()`.
`result['error']` contains the error description <br/>
`result['remaining_dp_budget']` contains the remaining differential
privacy budget when uber_dp is used. <br/>
"""
if self._vb:
print(f"Calling {__name__}.getAttack")
if self._attackCounter == 0:
# Caller shouldn't be calling if there are no expected
# answers, but is anyway, so just return
return {'query': {'sql': 'None'}, 'error': 'Nothing to do',
'stillToCome': 0}
job = self._attackQ.get()
self._attackQ.task_done()
self._attackCounter -= 1
reply = job['replies'][0]
reply['stillToCome'] = self._attackCounter
self._atrs['base']['attackGets'] += 1
if 'cells' in reply:
if reply['cells'] == 0:
self._atrs['base']['attackCells'] += 1
else:
self._atrs['base']['attackCells'] += reply['cells']
else:
self._atrs['base']['attackCells'] += 1
if self._type == 'uber_dp':
reply['remaining_dp_budget'] = self._remaining_dp_budget
return (reply)
def askKnowledge(self, query, cache=True):
""" Generate and queue up a prior knowledge query for database
The class keeps track of how many prior knowledge cells were
returned and uses this to compute a score. <br/>
Input parameters formatted the same as with `askAttack()`"""
self._knowledgeCounter += 1
if self._vb: print(f"Calling {__name__}.askKnowledge with query "
f"'{query}', count {self._knowledgeCounter}")
# Make a copy of the query for passing around
qCopy = copy.copy(query)
job = {}
job['q'] = self._knowledgeQ
qCopy['cache'] = cache
job['queries'] = [qCopy]
self._rawQ.put(job)
def getKnowledge(self):
""" Wait for and gather results of prior askKnowledge() calls
Blocks until the result is available. Note that the order
in which results are received is not necesarily the order
in which `askKnowledge()` calls were made. <br/>
Return parameter formatted the same as with `getAttack()`"""
if self._vb:
print(f"Calling {__name__}.getKnowledge")
if self._knowledgeCounter == 0:
# Caller shouldn't be calling if there are no expected
# answers, but is anyway, so just return
return {'query': {'sql': 'None'}, 'error': 'Nothing to do',
'stillToCome': 0}
job = self._knowledgeQ.get()
self._knowledgeQ.task_done()
self._knowledgeCounter -= 1
reply = job['replies'][0]
reply['stillToCome'] = self._knowledgeCounter
self._atrs['base']['knowledgeGets'] += 1
if 'cells' in reply:
self._atrs['base']['knowledgeCells'] += reply['cells']
return (reply)
def askExplore(self, query, cache=True):
""" Generate and queue up an exploritory query for database
No score book-keeping is done here. An analyst may make
any number of queries without impacting the GDA score. <br/>
`query` is a dictionary with two values: <br/>
`query['sql']` contains the SQL query. <br/>
`query['db']` determines which database is queried, and
is one of 'rawDb', 'anonDb', or (if linkability), 'pubDb'."""
self._exploreCounter += 1
if self._vb: print(f"Calling {__name__}.askExplore with "
f"query '{query}', count {self._exploreCounter}")
# Make a copy of the query for passing around
qCopy = copy.copy(query)
job = {}
job['q'] = self._exploreQ
qCopy['cache'] = cache
job['queries'] = [qCopy]
if qCopy['db'] == 'rawDb' or qCopy['db'] == 'raw':
self._rawQ.put(job)
elif qCopy['db'] == 'anonDb' or qCopy['db'] == 'anon':
self._anonQ.put(job)
else:
self._pubQ.put(job)
def getExplore(self):
""" Wait for and gather results of prior askExplore() calls.
Blocks until the result is available. Note that the order
in which results are received is not necesarily the order
in which `askExplore()` calls were made. <br/>
Return parameter formatted the same as with `getAttack()`"""
if self._vb:
print(f"Calling {__name__}.getExplore")
if self._exploreCounter == 0:
# Caller shouldn't be calling if there are no expected
# answers, but is anyway, so just return
return {'query': {'sql': 'None'}, 'error': 'Nothing to do',
'stillToCome': 0}
job = self._exploreQ.get()
self._exploreQ.task_done()
self._exploreCounter -= 1
reply = job['replies'][0]
reply['stillToCome'] = self._exploreCounter
return (reply)
def getPublicColValues(self, colName, tableName=''):
"""Return list of "publicly known" column values and counts
Column value has index 0, count of distinct UIDs has index 1
Must specify column name.
"""
if len(colName) == 0:
print(f"Must specify column 'colName'")
return None
if len(tableName) == 0:
# caller didn't supply a table name, so get it from the
# class init
tableName = self._p['table']
# Establish connection to database
db = getDatabaseInfo(self._p['rawDb'])
connStr = str(
f"host={db['host']} port={db['port']} dbname={db['dbname']} user={db['user']} password={db['password']}")
conn = psycopg2.connect(connStr)
cur = conn.cursor()
# First we need to know the total number of distinct users
sql = str(f"""select count(distinct {self._p['uid']})
from {tableName}""")
try:
cur.execute(sql)
except psycopg2.Error as e:
print(f"Error: getPublicColValues() query: '{e}'")
self.cleanUp(cleanUpCache=False, doExit=True)
ans = cur.fetchall()
numUid = ans[0][0]
# Query the raw db for values in the column
sql = str(f"""select {colName}, count(distinct {self._p['uid']})
from {tableName}
group by 1
order by 2 desc
limit 200""")
try:
cur.execute(sql)
except psycopg2.Error as e:
print(f"Error: getPublicColValues() query: '{e}'")
self.cleanUp(cleanUpCache=False, doExit=True)
ans = cur.fetchall()
ret = []
for row in ans:
# row[0] is the value, row[1] is the count
if (((row[1] / numUid) > 0.002) and
(row[1] >= 50)):
ret.append((row[0], row[1]))
conn.close()
return ret
def getColNames(self, dbType='rawDb', tableName=''):
"""Return simple list of column names
`dbType` is one of 'rawDb' or 'anonDb'"""
if len(tableName) == 0:
colsAndTypes = self.getColNamesAndTypes(dbType=dbType)
else:
colsAndTypes = self.getColNamesAndTypes(
dbType=dbType, tableName=tableName)
if not colsAndTypes:
return None
cols = []
for tup in colsAndTypes:
cols.append(tup[0])
return cols
def getAttackTableName(self):
"""Returns the name of the table being used in the attack."""
return self._p['table']
def getTableCharacteristics(self, tableName=''):
"""Returns the full contents of the table characteristics
Return value is a dict indexed by column name: <br/>
{ '<colName>':
{
'av_rows_per_vals': 3.93149,
'av_uids_per_val': 0.468698,
'column_label': 'continuous',
'column_name': 'dropoff_latitude',
'column_type': 'real',
'max': '898.29382000000000',
'min': '-0.56333297000000',
'num_distinct_vals': 24216,
'num_rows': 95205,
'num_uids': 11350,
'std_rows_per_val': 10.8547,
'std_uids_per_val': 4.09688},
}
}
"""
if len(tableName) == 0:
# caller didn't supply a table name, so get it from the
# class init
tableName = self._p['table']
# Modify table name to the default for the characteristics table
tableName += '_char'
# Establish connection to database
db = getDatabaseInfo(self._p['rawDb'])
connStr = str(
f"host={db['host']} port={db['port']} dbname={db['dbname']} user={db['user']} password={db['password']}")
conn = psycopg2.connect(connStr)
cur = conn.cursor()
# Set up return dict
ret = {}
# Query it for column names
sql = str(f"""select column_name, data_type
from information_schema.columns where
table_name='{tableName}'""")
try:
cur.execute(sql)
except psycopg2.Error as e:
print(f"Error: getTableCharacteristics() query: '{e}'")
self.cleanUp(cleanUpCache=False, doExit=True)
cols = cur.fetchall()
# Make index for column name (should be 0, but just to be sure)
for colNameIndex in range(len(cols)):
if cols[colNameIndex][0] == 'column_name':
break
# Query it for table contents
sql = str(f"SELECT * FROM {tableName}")
try:
cur.execute(sql)
except psycopg2.Error as e:
print(f"Error: getTableCharacteristics() query: '{e}'")
self.cleanUp(cleanUpCache=False, doExit=True)
ans = cur.fetchall()
for row in ans:
colName = row[colNameIndex]
ret[colName] = {}
for i in range(len(row)):
ret[colName][cols[i][0]] = row[i]
conn.close()
return ret
def getAnonTableCharacteristics(self, tableName=''):
"""Returns the full contents of the table characteristics
Return value is a dict indexed by column name: <br/>
{ '<colName>':
{
'av_rows_per_vals': 3.93149,
'av_uids_per_val': 0.468698,
'column_label': 'continuous',
'column_name': 'dropoff_latitude',
'column_type': 'real',
'max': '898.29382000000000',
'min': '-0.56333297000000',
'num_distinct_vals': 24216,
'num_rows': 95205,
'num_uids': 11350,
'std_rows_per_val': 10.8547,
'std_uids_per_val': 4.09688},
}
}
"""
if len(tableName) == 0:
# caller didn't supply a table name, so get it from the
# class init
tableName = self._p['table']
# Modify table name to the default for the characteristics table
# tableName += '_char'
# Establish connection to database
db = getDatabaseInfo(self._p['anonDb'])
connStr = str(
f"host={db['host']} port={db['port']} dbname={db['dbname']} user={db['user']} password={db['password']}")
conn = psycopg2.connect(connStr)
cur = conn.cursor()
# Query it for column names
sql = str(f"""select column_name, data_type
from information_schema.columns
where table_schema NOT IN ('information_schema', 'pg_catalog') and
table_name='{tableName}'""")
try:
cur.execute(sql)
except psycopg2.Error as e:
print(f"Error: getAnonTableCharacteristics() query: '{e}'")
self.cleanUp(cleanUpCache=False, doExit=True)
ans = cur.fetchall()
# Set up return dict
ret = {_row[0]: {'column_name': _row[0], 'column_type': _row[1]} for _row in ans}
conn.close()
return ret
# Note that following is used internally, but we expose it to the
# caller as well because it is a useful function for exploration
def getColNamesAndTypes(self, dbType='rawDb', tableName=''):
"""Return raw database column names and types (or None if error)
dbType is one of 'rawDb' or 'anonDb' <br/>
return format: [(col,type),(col,type),...]"""
if len(tableName) == 0:
# caller didn't supply a table name, so get it from the
# class init
tableName = self._p['table']
# Establish connection to database
db = getDatabaseInfo(self._p[dbType])
if db['type'] != 'postgres' and db['type'] != 'aircloak':
print(f"DB type '{db['type']}' must be 'postgres' or 'aircloak'")
return None
connStr = str(
f"host={db['host']} port={db['port']} dbname={db['dbname']} user={db['user']} password={db['password']}")
conn = psycopg2.connect(connStr)
cur = conn.cursor()
# Query it for column names
if db['type'] == 'postgres':
sql = str(f"""select column_name, data_type
from information_schema.columns where
table_name='{tableName}'""")
elif db['type'] == 'aircloak':
sql = str(f"show columns from {tableName}")
try:
cur.execute(sql)
except psycopg2.Error as e:
print(f"Error: getColNamesAndTypes() query: '{e}'")
self.cleanUp(cleanUpCache=False, doExit=True)
ans = cur.fetchall()
ret = []
for row in ans:
ret.append((row[0], row[1]))
conn.close()
return ret
def getTableNames(self, dbType='rawDb'):
"""Return database table names
dbType is one of 'rawDb' or 'anonDb' <br/>
Table names returned as list, unless error then return None"""
# Establish connection to database
db = getDatabaseInfo(self._p[dbType])
if db['type'] != 'postgres' and db['type'] != 'aircloak':
print(f"DB type '{db['type']}' must be 'postgres' or 'aircloak'")
return None
connStr = str(
f"host={db['host']} port={db['port']} dbname={db['dbname']} user={db['user']} password={db['password']}")
conn = psycopg2.connect(connStr)
cur = conn.cursor()
# Query it for column names
if db['type'] == 'postgres':
sql = """SELECT tablename
FROM pg_catalog.pg_tables
WHERE schemaname != 'pg_catalog' AND
schemaname != 'information_schema'"""
elif db['type'] == 'aircloak':
sql = "show tables"
try:
cur.execute(sql)
except psycopg2.Error as e:
print(f"Error: getTableNames() query: '{e}'")
self.cleanUp(cleanUpCache=False, doExit=True)
ans = cur.fetchall()
ret = []
for row in ans:
ret.append(row[0])
conn.close()
return ret
def getUidColName(self):
""" Returns the name of the UID column"""
return self._p['uid']
def getPriorKnowledge(self, dataColumns, method,
fraction=None, count=None, selectColumn=None, colRange=[None,None], values=[None]):
""" Returns data from the rawDB according to a specification
This mimics external knowledge that an attacker may have about the data, and
influences the 'knowledge' part of the GDA Score. <br/>
`dataColumns` is a list of column names. The data for these columns is returned <br/>
`method` can be 'rows' or 'users'. If 'rows', then rows are selected
according to the criteria (`fraction`, `count`, `selectColumn`, `colRange`,
or `values`).
If 'users', then all rows for a set of selected users is returned.
The users are selected according to the criteria (`fraction` or `count`) <br/>
If none of the criteria are set, or if `fraction` is set to 1.0, then all
rows are returned (for the selected column values) One of `fraction`, `count`,
or `selectColumn` must be set. <br/>
`fraction` or `count` are set to obtain a random set of rows or users. If
`fraction`, then an approximate fraction of all rows/users is selected.
`fraction` is a value between 0 and 1.0. If `count`, then exactly `count`
random rows/users are selected. <br/>
`selectColumn` is set to select rows according to the values of the specified
column. `selectColumn` is a column name. If set, then either a range of
values (`colRange`), or a set of values (`values`) must be chosen. <br/>
`colRange` is
a list with two values: `[min,max]`. This selects all values
between min and max inclusive. <br/>
`values` is a list
of one or more values of any type. This selects all values matching those in
the list. <br/>
The return value is a list in this format: <br/>
`[(C1,C2...,Cn),(C1,C2...,Cn), ... (C1,C2...,Cn)]` <br/>
where C1 corresponds to the first column in `dataColumns`, C2 corresponds to
the second column in `dataColumns`, and so on. <br/>
"""
# Check input parameters
if not isinstance(dataColumns, list):
print(f"getPriorKnowledge Error: dataColumns must be a list of one or more column names")
self.cleanUp(cleanUpCache=False, doExit=True)
if method not in ['rows','users']:
print(f"getPriorKnowledge Error: method must be 'rows' or 'users'")
self.cleanUp(cleanUpCache=False, doExit=True)
if fraction is None and count is None and selectColumn is None:
print(f"getPriorKnowledge Error: one of fraction, count, or selectColumn must be set")
self.cleanUp(cleanUpCache=False, doExit=True)
if fraction and not isinstance(fraction, float):
print(f"getPriorKnowledge Error: if set, fraction must be a float")
self.cleanUp(cleanUpCache=False, doExit=True)
if (fraction and (count or selectColumn)) or (count and (fraction or selectColumn)):
print(f"getPriorKnowledge Error: only one of fraction, count, or selectColumn may be set")
self.cleanUp(cleanUpCache=False, doExit=True)
if count and not isinstance(count, int):
print(f"getPriorKnowledge Error: if set, count must be an integer")
self.cleanUp(cleanUpCache=False, doExit=True)
if selectColumn:
if selectColumn not in self._colNames:
print(f"getPriorKnowledge Error: selectColumn '{selectColumn}' is not a valid column")
self.cleanUp(cleanUpCache=False, doExit=True)
if colRange == [None,None] and values == [None]:
print(f"getPriorKnowledge Error: if selectColumn is set, one of colRange or values must be set")
self.cleanUp(cleanUpCache=False, doExit=True)
if not isinstance(colRange, list):
print(f"getPriorKnowledge Error: colRange must be a list with two values")