/
utils.py
380 lines (295 loc) · 11.6 KB
/
utils.py
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
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# SPDX-License-Identifier: Apache-2.0
import ast
import logging
import os.path
import sys
try:
import configparser
except ImportError:
import ConfigParser as configparser
LOG = logging.getLogger(__name__)
"""Various helper functions."""
def _get_attr_qual_name(node, aliases):
"""Get a the full name for the attribute node.
This will resolve a pseudo-qualified name for the attribute
rooted at node as long as all the deeper nodes are Names or
Attributes. This will give you how the code referenced the name but
will not tell you what the name actually refers to. If we
encounter a node without a static name we punt with an
empty string. If this encounters something more complex, such as
foo.mylist[0](a,b) we just return empty string.
:param node: AST Name or Attribute node
:param aliases: Import aliases dictionary
:returns: Qualified name referred to by the attribute or name.
"""
if isinstance(node, ast.Name):
if node.id in aliases:
return aliases[node.id]
return node.id
elif isinstance(node, ast.Attribute):
name = f"{_get_attr_qual_name(node.value, aliases)}.{node.attr}"
if name in aliases:
return aliases[name]
return name
else:
return ""
def get_call_name(node, aliases):
if isinstance(node.func, ast.Name):
if deepgetattr(node, "func.id") in aliases:
return aliases[deepgetattr(node, "func.id")]
return deepgetattr(node, "func.id")
elif isinstance(node.func, ast.Attribute):
return _get_attr_qual_name(node.func, aliases)
else:
return ""
def get_func_name(node):
return node.name # TODO(tkelsey): get that qualname using enclosing scope
def get_qual_attr(node, aliases):
prefix = ""
if isinstance(node, ast.Attribute):
try:
val = deepgetattr(node, "value.id")
if val in aliases:
prefix = aliases[val]
else:
prefix = deepgetattr(node, "value.id")
except Exception:
# NOTE(tkelsey): degrade gracefully when we can't get the fully
# qualified name for an attr, just return its base name.
pass
return f"{prefix}.{node.attr}"
else:
return "" # TODO(tkelsey): process other node types
def deepgetattr(obj, attr):
"""Recurses through an attribute chain to get the ultimate value."""
for key in attr.split("."):
obj = getattr(obj, key)
return obj
class InvalidModulePath(Exception):
pass
class ConfigError(Exception):
"""Raised when the config file fails validation."""
def __init__(self, message, config_file):
self.config_file = config_file
self.message = f"{config_file} : {message}"
super().__init__(self.message)
class ProfileNotFound(Exception):
"""Raised when chosen profile cannot be found."""
def __init__(self, config_file, profile):
self.config_file = config_file
self.profile = profile
message = "Unable to find profile ({}) in config file: {}".format(
self.profile,
self.config_file,
)
super().__init__(message)
def warnings_formatter(
message, category=UserWarning, filename="", lineno=-1, line=""
):
"""Monkey patch for warnings.warn to suppress cruft output."""
return f"{message}\n"
def get_module_qualname_from_path(path):
"""Get the module's qualified name by analysis of the path.
Resolve the absolute pathname and eliminate symlinks. This could result in
an incorrect name if symlinks are used to restructure the python lib
directory.
Starting from the right-most directory component look for __init__.py in
the directory component. If it exists then the directory name is part of
the module name. Move left to the subsequent directory components until a
directory is found without __init__.py.
:param: Path to module file. Relative paths will be resolved relative to
current working directory.
:return: fully qualified module name
"""
(head, tail) = os.path.split(path)
if head == "" or tail == "":
raise InvalidModulePath(
'Invalid python file path: "%s"'
" Missing path or file name" % (path)
)
qname = [os.path.splitext(tail)[0]]
while head not in ["/", ".", ""]:
if os.path.isfile(os.path.join(head, "__init__.py")):
(head, tail) = os.path.split(head)
qname.insert(0, tail)
else:
break
qualname = ".".join(qname)
return qualname
def namespace_path_join(base, name):
"""Extend the current namespace path with an additional name
Take a namespace path (i.e., package.module.class) and extends it
with an additional name (i.e., package.module.class.subclass).
This is similar to how os.path.join works.
:param base: (String) The base namespace path.
:param name: (String) The new name to append to the base path.
:returns: (String) A new namespace path resulting from combination of
base and name.
"""
return f"{base}.{name}"
def namespace_path_split(path):
"""Split the namespace path into a pair (head, tail).
Tail will be the last namespace path component and head will
be everything leading up to that in the path. This is similar to
os.path.split.
:param path: (String) A namespace path.
:returns: (String, String) A tuple where the first component is the base
path and the second is the last path component.
"""
return tuple(path.rsplit(".", 1))
def escaped_bytes_representation(b):
"""PY3 bytes need escaping for comparison with other strings.
In practice it turns control characters into acceptable codepoints then
encodes them into bytes again to turn unprintable bytes into printable
escape sequences.
This is safe to do for the whole range 0..255 and result matches
unicode_escape on a unicode string.
"""
return b.decode("unicode_escape").encode("unicode_escape")
def calc_linerange(node):
"""Calculate linerange for subtree"""
if hasattr(node, "_bandit_linerange"):
return node._bandit_linerange
lines_min = 9999999999
lines_max = -1
if hasattr(node, "lineno"):
lines_min = node.lineno
lines_max = node.lineno
for n in ast.iter_child_nodes(node):
lines_minmax = calc_linerange(n)
lines_min = min(lines_min, lines_minmax[0])
lines_max = max(lines_max, lines_minmax[1])
node._bandit_linerange = (lines_min, lines_max)
return (lines_min, lines_max)
def linerange(node):
"""Get line number range from a node."""
if sys.version_info >= (3, 8) and hasattr(node, "lineno"):
return list(range(node.lineno, node.end_lineno + 1))
else:
if hasattr(node, "_bandit_linerange_stripped"):
lines_minmax = node._bandit_linerange_stripped
return list(range(lines_minmax[0], lines_minmax[1] + 1))
strip = {
"body": None,
"orelse": None,
"handlers": None,
"finalbody": None,
}
for key in strip.keys():
if hasattr(node, key):
strip[key] = getattr(node, key)
setattr(node, key, [])
lines_min = 9999999999
lines_max = -1
if hasattr(node, "lineno"):
lines_min = node.lineno
lines_max = node.lineno
for n in ast.iter_child_nodes(node):
lines_minmax = calc_linerange(n)
lines_min = min(lines_min, lines_minmax[0])
lines_max = max(lines_max, lines_minmax[1])
for key in strip.keys():
if strip[key] is not None:
setattr(node, key, strip[key])
if lines_max == -1:
lines_min = 0
lines_max = 1
node._bandit_linerange_stripped = (lines_min, lines_max)
lines = list(range(lines_min, lines_max + 1))
"""Try and work around a known Python bug with multi-line strings."""
# deal with multiline strings lineno behavior (Python issue #16806)
if hasattr(node, "_bandit_sibling") and hasattr(
node._bandit_sibling, "lineno"
):
start = min(lines)
delta = node._bandit_sibling.lineno - start
if delta > 1:
return list(range(start, node._bandit_sibling.lineno))
return lines
def concat_string(node, stop=None):
"""Builds a string from a ast.BinOp chain.
This will build a string from a series of ast.Str nodes wrapped in
ast.BinOp nodes. Something like "a" + "b" + "c" or "a %s" % val etc.
The provided node can be any participant in the BinOp chain.
:param node: (ast.Str or ast.BinOp) The node to process
:param stop: (ast.Str or ast.BinOp) Optional base node to stop at
:returns: (Tuple) the root node of the expression, the string value
"""
def _get(node, bits, stop=None):
if node != stop:
bits.append(
_get(node.left, bits, stop)
if isinstance(node.left, ast.BinOp)
else node.left
)
bits.append(
_get(node.right, bits, stop)
if isinstance(node.right, ast.BinOp)
else node.right
)
bits = [node]
while isinstance(node._bandit_parent, ast.BinOp):
node = node._bandit_parent
if isinstance(node, ast.BinOp):
_get(node, bits, stop)
return (node, " ".join([x.s for x in bits if isinstance(x, ast.Str)]))
def get_called_name(node):
"""Get a function name from an ast.Call node.
An ast.Call node representing a method call with present differently to one
wrapping a function call: thing.call() vs call(). This helper will grab the
unqualified call name correctly in either case.
:param node: (ast.Call) the call node
:returns: (String) the function name
"""
func = node.func
try:
return func.attr if isinstance(func, ast.Attribute) else func.id
except AttributeError:
return ""
def get_path_for_function(f):
"""Get the path of the file where the function is defined.
:returns: the path, or None if one could not be found or f is not a real
function
"""
if hasattr(f, "__module__"):
module_name = f.__module__
elif hasattr(f, "im_func"):
module_name = f.im_func.__module__
else:
LOG.warning("Cannot resolve file where %s is defined", f)
return None
module = sys.modules[module_name]
if hasattr(module, "__file__"):
return module.__file__
else:
LOG.warning("Cannot resolve file path for module %s", module_name)
return None
def parse_ini_file(f_loc):
config = configparser.ConfigParser()
try:
config.read(f_loc)
return {k: v for k, v in config.items("bandit")}
except (configparser.Error, KeyError, TypeError):
LOG.warning(
"Unable to parse config file %s or missing [bandit] " "section",
f_loc,
)
return None
def check_ast_node(name):
"Check if the given name is that of a valid AST node."
try:
node = getattr(ast, name)
if issubclass(node, ast.AST):
return name
except AttributeError: # nosec(tkelsey): catching expected exception
pass
raise TypeError("Error: %s is not a valid node type in AST" % name)
def get_nosec(nosec_lines, context):
for lineno in context["linerange"]:
nosec = nosec_lines.get(lineno, None)
if nosec is not None:
return nosec
return None