-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathattacks.py
More file actions
242 lines (186 loc) · 8.39 KB
/
attacks.py
File metadata and controls
242 lines (186 loc) · 8.39 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
import string
from nltk.corpus import stopwords as SW
from collections import defaultdict
import random
from random import shuffle
import numpy as np
punctuations = string.punctuation + ' '
stopwords = set(SW.words("english")) | set(string.punctuation)
keyboard_mappings = None
MIN_LEN = 5
def drop_one_attack_deprecated(line):
for i in range(1, len(line) - 1):
if line[i] in punctuations or line[i-1] in punctuations \
or line[i+1] in punctuations:
# first or last character of a word
continue
# drop the ith character
new_line = line[:i] + line[i+1:]
if len(new_line.split()) != len(line.split()):
# probably dropped a single char word
continue
yield new_line
def drop_one_attack(line, ignore_indices = set(), include_ends=False):
""" an attack that drops one character at a time
Arguments:
line {string} -- the input line/review/comment
Keyword Arguments:
ignore_idx {int} -- ignores a given set of indices
include_ends {bool} -- to include dropping the first & last char?
"""
words = line.split()
for idx, word in enumerate(words):
if len(word) < 3: continue
if word in stopwords: continue
if idx in ignore_indices: continue
if include_ends:
adversary_words = [word[:i] + word[i+1:] for i in range(0, len(word))]
else:
adversary_words = [word[:i] + word[i+1:] for i in range(1, len(word)-1)]
for adv in adversary_words:
yield idx, " ".join(words[:idx] + [adv] + words[idx+1:])
def swap_one_attack(line, ignore_indices = set(), include_ends=False):
""" an attack that drops one character at a time
Arguments:
line {string} -- the input line/review/comment
Keyword Arguments:
ignore_idx {int} -- ignores a given set of indices
include_ends {bool} -- to include dropping the first & last char?
"""
words = line.split()
for idx, word in enumerate(words):
if len(word) < MIN_LEN: continue
if word in stopwords: continue
if idx in ignore_indices: continue
if include_ends:
adversary_words = [word[:i] + word[i:i+2][::-1] + word[i+2:] for i in range(0, len(word)-1)]
else:
adversary_words = [word[:i] + word[i:i+2][::-1] + word[i+2:] for i in range(1, len(word)-2)]
for adv in adversary_words:
yield idx, " ".join(words[:idx] + [adv] + words[idx+1:])
def add_one_attack(line, ignore_indices = set(), include_ends=False,
alphabets="abcdefghijklmnopqrstuvwxyz"):
""" an attack that adds one random character in the line
Arguments:
line {string} -- the input line/review/comment
Keyword Arguments:
ignore_idx {int} -- ignores a given set of indices
include_ends {bool} -- include the first & last char for adddition?
"""
words = line.split()
#alphabets = "abcdefghijklmnopqrstuvwxyz"
alphabets = [i for i in alphabets]
for idx, word in enumerate(words):
if len(word) < 3: continue # need to have this same as defense settings
if word in stopwords: continue
if idx in ignore_indices: continue
if include_ends:
adversary_words = [word[:i] + alpha + word[i:] for i in range(0, len(word) + 1) \
for alpha in alphabets]
else:
adversary_words = [word[:i] + alpha + word[i:] for i in range(1, len(word)) \
for alpha in alphabets]
for adv in adversary_words:
yield idx, " ".join(words[:idx] + [adv] + words[idx+1:])
def key_one_attack(line, ignore_indices = set(), include_ends=False):
""" an attack that adds one random character in the line
Arguments:
line {string} -- the input line/review/comment
Keyword Arguments:
ignore_idx {int} -- ignores a given set of indices
include_ends {bool} -- include the first & last char for adddition?
"""
words = line.split()
for idx, word in enumerate(words):
if len(word) < 3: continue # need to have this same as defense settings
if word in stopwords: continue
if idx in ignore_indices: continue
adversary_words = []
if include_ends:
for i in range(0, len(word)):
for key in get_keyboard_neighbors(word[i]):
adversary_words.append(word[:i] + key + word[i+1:])
else:
for i in range(1, len(word) - 1):
for key in get_keyboard_neighbors(word[i]):
adversary_words.append(word[:i] + key + word[i+1:])
for adv in adversary_words:
yield idx, " ".join(words[:idx] + [adv] + words[idx+1:])
# this is the all attack setting, where all of the add/swap/drop/key
# attacks are tried
def all_one_attack(line, ignore_indices = set(), include_ends=False,
alphabets="abcdefghijklmnopqrstuvwxyz"):
generator = add_one_attack(line, ignore_indices, include_ends,
alphabets=alphabets)
for idx, adv in generator:
yield idx, adv
generator = key_one_attack(line, ignore_indices, include_ends)
for idx, adv in generator:
yield idx, adv
generator = drop_one_attack(line, ignore_indices, include_ends)
for idx, adv in generator:
yield idx, adv
generator = swap_one_attack(line, ignore_indices, include_ends)
for idx, adv in generator:
yield idx, adv
def random_all_one_attack(line, ignore_indices=set(), include_ends=False):
generators = [add_one_attack, key_one_attack, drop_one_attack, swap_one_attack]
shuffle(generators)
for generator in generators:
for idx, adv in generator(line, ignore_indices, include_ends):
yield idx, adv
def get_keyboard_neighbors(ch):
global keyboard_mappings
if keyboard_mappings is None or len(keyboard_mappings) != 26:
keyboard_mappings = defaultdict(lambda: [])
keyboard = ["qwertyuiop", "asdfghjkl*", "zxcvbnm***"]
row = len(keyboard)
col = len(keyboard[0])
dx = [-1, 1, 0, 0]
dy = [0, 0, -1, 1]
for i in range(row):
for j in range(col):
for k in range(4):
x_, y_ = i + dx[k], j + dy[k]
if (x_ >= 0 and x_ < row) and (y_ >= 0 and y_ < col):
if keyboard[x_][y_] == '*': continue
if keyboard[i][j] == '*': continue
keyboard_mappings[keyboard[i][j]].append(keyboard[x_][y_])
if ch not in keyboard_mappings: return [ch]
return keyboard_mappings[ch]
def is_valid_attack(line, char_idx):
line = line.lower()
if char_idx == 0 or char_idx == len(line) - 1:
# first and last chars of the sentence
return False
if line[char_idx-1] == ' ' or line[char_idx+1] == ' ':
# first and last chars of the word
return False
# anything not a legit alphabet
if not('a' <= line[char_idx] <= 'z'):
return False
return True
def get_random_attack(line):
num_chars = len(line)
NUM_TRIES = 10
for _ in range(NUM_TRIES):
char_idx = np.random.choice(range(num_chars), 1)[0]
if is_valid_attack(line, char_idx):
attack_type = ['swap', 'drop', 'add', 'key']
attack_probs = np.array([1.0, 1.0, 10.0, 2.0])
attack_probs = attack_probs/sum(attack_probs)
attack = np.random.choice(attack_type, 1, p=attack_probs)[0]
if attack == 'swap':
return line[:char_idx] + line[char_idx:char_idx+2][::-1] + line[char_idx+2:]
elif attack == 'drop':
return line[:char_idx] + line[char_idx+1:]
elif attack == 'key':
sideys = get_keyboard_neighbors(line[char_idx])
new_ch = np.random.choice(sideys, 1)[0]
return line[:char_idx] + new_ch + line[char_idx+1:]
else: # attack type is add
alphabets = "abcdefghijklmnopqrstuvwxyz"
alphabets = [ch for ch in alphabets]
new_ch = np.random.choice(alphabets, 1)[0]
return line[:char_idx] + new_ch + line[char_idx:]
return None