|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a context class for security related checks. |
|
8 """ |
|
9 |
|
10 # |
|
11 # This code is a modified version of the one in 'bandit'. |
|
12 # |
|
13 # Original Copyright 2014 Hewlett-Packard Development Company, L.P. |
|
14 # |
|
15 # SPDX-License-Identifier: Apache-2.0 |
|
16 # |
|
17 |
|
18 import ast |
|
19 import sys |
|
20 |
|
21 from . import SecurityUtils |
|
22 |
|
23 |
|
24 class SecurityContext(object): |
|
25 """ |
|
26 Class implementing a context class for security related checks. |
|
27 """ |
|
28 def __init__(self, contextObject=None): |
|
29 """ |
|
30 Constructor |
|
31 |
|
32 Initialize the class with a context dictionary or an empty |
|
33 dictionary. |
|
34 |
|
35 @param contextObject context dictionary to be used to populate the |
|
36 class |
|
37 @type dict |
|
38 """ |
|
39 if contextObject is not None: |
|
40 self.__context = contextObject |
|
41 else: |
|
42 self.__context = {} |
|
43 |
|
44 def __repr__(self): |
|
45 """ |
|
46 Private method to generate representation of object for printing or |
|
47 interactive use. |
|
48 |
|
49 @return string representation of the object |
|
50 @rtype str |
|
51 """ |
|
52 return "<SecurityContext {0}>".formar(self.__context) |
|
53 |
|
54 @property |
|
55 def callArgs(self): |
|
56 """ |
|
57 Public method to get a list of function args. |
|
58 |
|
59 @return list of function args |
|
60 @rtype list |
|
61 """ |
|
62 args = [] |
|
63 if ( |
|
64 'call' in self.__context and |
|
65 hasattr(self.__context['call'], 'args') |
|
66 ): |
|
67 for arg in self.__context['call'].args: |
|
68 if hasattr(arg, 'attr'): |
|
69 args.append(arg.attr) |
|
70 else: |
|
71 args.append(self.__getLiteralValue(arg)) |
|
72 return args |
|
73 |
|
74 @property |
|
75 def callArgsCount(self): |
|
76 """ |
|
77 Public method to get the number of args a function call has. |
|
78 |
|
79 @return number of args a function call has |
|
80 @rtype int |
|
81 """ |
|
82 if 'call' in self.__context and hasattr(self.__context['call'], 'args'): |
|
83 return len(self.__context['call'].args) |
|
84 else: |
|
85 return None |
|
86 |
|
87 @property |
|
88 def callFunctionName(self): |
|
89 """ |
|
90 Public method to get the name (not FQ) of a function call. |
|
91 |
|
92 @return name (not FQ) of a function call |
|
93 @rtype str |
|
94 """ |
|
95 return self.__context.get('name') |
|
96 |
|
97 @property |
|
98 def callFunctionNameQual(self): |
|
99 """ |
|
100 Public method to get the FQ name of a function call. |
|
101 |
|
102 @return FQ name of a function call |
|
103 @rtype str |
|
104 """ |
|
105 return self.__context.get('qualname') |
|
106 |
|
107 @property |
|
108 def callKeywords(self): |
|
109 """ |
|
110 Public method to get a dictionary of keyword parameters. |
|
111 |
|
112 @return dictionary of keyword parameters |
|
113 @rtype dict |
|
114 """ |
|
115 if ( |
|
116 'call' in self.__context and |
|
117 hasattr(self.__context['call'], 'keywords') |
|
118 ): |
|
119 returnDict = {} |
|
120 for kw in self.__context['call'].keywords: |
|
121 if hasattr(kw.value, 'attr'): |
|
122 returnDict[kw.arg] = kw.value.attr |
|
123 else: |
|
124 returnDict[kw.arg] = self.__getLiteralValue(kw.value) |
|
125 return returnDict |
|
126 |
|
127 else: |
|
128 return None |
|
129 |
|
130 @property |
|
131 def node(self): |
|
132 """ |
|
133 Public method to get the raw AST node associated with the context. |
|
134 |
|
135 @return raw AST node associated with the context |
|
136 @rtype ast.AST |
|
137 """ |
|
138 return self.__context.get('node') |
|
139 |
|
140 @property |
|
141 def stringVal(self): |
|
142 """ |
|
143 Public method to get the value of a standalone unicode or string |
|
144 object. |
|
145 |
|
146 @return value of a standalone unicode or string object |
|
147 @rtype str |
|
148 """ |
|
149 return self.__context.get('str') |
|
150 |
|
151 @property |
|
152 def bytesVal(self): |
|
153 """ |
|
154 Public method to get the value of a standalone bytes object. |
|
155 |
|
156 @return value of a standalone bytes object |
|
157 @rtype bytes |
|
158 """ |
|
159 return self.__context.get('bytes') |
|
160 |
|
161 @property |
|
162 def stringValAsEscapedBytes(self): |
|
163 r""" |
|
164 Public method to get the escaped value of the object. |
|
165 |
|
166 Turn the value of a string or bytes object into a byte sequence with |
|
167 unknown, control, and \\ characters escaped. |
|
168 |
|
169 This function should be used when looking for a known sequence in a |
|
170 potentially badly encoded string in the code. |
|
171 |
|
172 @return sequence of printable ascii bytes representing original string |
|
173 @rtype str |
|
174 """ |
|
175 val = self.stringVal |
|
176 if val is not None: |
|
177 # it's any of str or unicode in py2, or str in py3 |
|
178 return val.encode('unicode_escape') |
|
179 |
|
180 val = self.bytesVal |
|
181 if val is not None: |
|
182 return SecurityUtils.escapedBytesRepresentation(val) |
|
183 |
|
184 return None |
|
185 |
|
186 @property |
|
187 def statement(self): |
|
188 """ |
|
189 Public method to get the raw AST for the current statement. |
|
190 |
|
191 @return raw AST for the current statement |
|
192 @rtype ast.AST |
|
193 """ |
|
194 return self.__context.get('statement') |
|
195 |
|
196 @property |
|
197 def functionDefDefaultsQual(self): |
|
198 """ |
|
199 Public method to get a list of fully qualified default values in a |
|
200 function def. |
|
201 |
|
202 @return list of fully qualified default values in a function def |
|
203 @rtype list |
|
204 """ |
|
205 defaults = [] |
|
206 if ( |
|
207 'node' in self.__context and |
|
208 hasattr(self.__context['node'], 'args') and |
|
209 hasattr(self.__context['node'].args, 'defaults') |
|
210 ): |
|
211 for default in self.__context['node'].args.defaults: |
|
212 defaults.append(SecurityUtils.getQualAttr( |
|
213 default, |
|
214 self.__context['import_aliases'])) |
|
215 |
|
216 return defaults |
|
217 |
|
218 def __getLiteralValue(self, literal): |
|
219 """ |
|
220 Private method to turn AST literals into native Python types. |
|
221 |
|
222 @param literal AST literal to be converted |
|
223 @type ast.AST |
|
224 @return converted Python object |
|
225 @rtype Any |
|
226 """ |
|
227 if isinstance(literal, ast.Num): |
|
228 literalValue = literal.n |
|
229 |
|
230 elif isinstance(literal, ast.Str): |
|
231 literalValue = literal.s |
|
232 |
|
233 elif isinstance(literal, ast.List): |
|
234 returnList = list() |
|
235 for li in literal.elts: |
|
236 returnList.append(self.__getLiteralValue(li)) |
|
237 literalValue = returnList |
|
238 |
|
239 elif isinstance(literal, ast.Tuple): |
|
240 returnTuple = tuple() |
|
241 for ti in literal.elts: |
|
242 returnTuple = returnTuple + (self.__getLiteralValue(ti),) |
|
243 literalValue = returnTuple |
|
244 |
|
245 elif isinstance(literal, ast.Set): |
|
246 returnSet = set() |
|
247 for si in literal.elts: |
|
248 returnSet.add(self.__getLiteralValue(si)) |
|
249 literalValue = returnSet |
|
250 |
|
251 elif isinstance(literal, ast.Dict): |
|
252 literalValue = dict(zip(literal.keys, literal.values)) |
|
253 |
|
254 elif isinstance(literal, ast.Ellipsis): |
|
255 # what do we want to do with this? |
|
256 literalValue = None |
|
257 |
|
258 elif isinstance(literal, ast.Name): |
|
259 literalValue = literal.id |
|
260 |
|
261 # NameConstants are only part of the AST in Python 3. NameConstants |
|
262 # tend to refer to things like True and False. This prevents them from |
|
263 # being re-assigned in Python 3. |
|
264 elif ( |
|
265 sys.version_info >= (3, 0, 0) and |
|
266 isinstance(literal, ast.NameConstant) |
|
267 ): |
|
268 literalValue = str(literal.value) |
|
269 |
|
270 # Bytes are only part of the AST in Python 3 |
|
271 elif ( |
|
272 sys.version_info >= (3, 0, 0) and |
|
273 isinstance(literal, ast.Bytes) |
|
274 ): |
|
275 literalValue = literal.s |
|
276 |
|
277 else: |
|
278 literalValue = None |
|
279 |
|
280 return literalValue |
|
281 |
|
282 def getCallArgValue(self, argumentName): |
|
283 """ |
|
284 Public method to get the value of a named argument in a function call. |
|
285 |
|
286 @param argumentName name of the argument to get the value for |
|
287 @type str |
|
288 @return value of the named argument |
|
289 @rtype Any |
|
290 """ |
|
291 kwdValues = self.callKeywords |
|
292 if kwdValues is not None and argumentName in kwdValues: |
|
293 return kwdValues[argumentName] |
|
294 |
|
295 def checkCallArgValue(self, argumentName, argumentValues=None): |
|
296 """ |
|
297 Public method to check for a value of a named argument in a function |
|
298 call. |
|
299 |
|
300 @param argumentName name of the argument to be checked |
|
301 @type str |
|
302 @param argumentValues value or list of values to test against |
|
303 @type Any or list of Any |
|
304 @return True if argument found and matched, False if found and not |
|
305 matched, None if argument not found at all |
|
306 @rtype bool or None |
|
307 """ |
|
308 argValue = self.getCallArgValue(argumentName) |
|
309 if argValue is not None: |
|
310 if not isinstance(argumentValues, list): |
|
311 # if passed a single value, or a tuple, convert to a list |
|
312 argumentValues = list((argumentValues,)) |
|
313 for val in argumentValues: |
|
314 if argValue == val: |
|
315 return True |
|
316 return False |
|
317 else: |
|
318 # argument name not found, return None to allow testing for this |
|
319 # eventuality |
|
320 return None |
|
321 |
|
322 def getLinenoForCallArg(self, argumentName): |
|
323 """ |
|
324 Public method to get the line number for a specific named argument. |
|
325 |
|
326 @param argumentName name of the argument to get the line number for |
|
327 @type str |
|
328 @return line number of the found argument or -1 |
|
329 @rtype int |
|
330 """ |
|
331 if hasattr(self.node, 'keywords'): |
|
332 for key in self.node.keywords: |
|
333 if key.arg == argumentName: |
|
334 return key.value.lineno |
|
335 |
|
336 def getCallArgAtPosition(self, positionNum): |
|
337 """ |
|
338 Public method to get a positional argument at the specified position |
|
339 (if it exists). |
|
340 |
|
341 @param positionNum index of the argument to get the value for |
|
342 @type int |
|
343 @return value of the argument at the specified position if it exists |
|
344 @rtype Any or None |
|
345 """ |
|
346 maxArgs = self.callArgsCount |
|
347 if maxArgs and positionNum < maxArgs: |
|
348 return self.__getLiteralValue( |
|
349 self.__context['call'].args[positionNum] |
|
350 ) |
|
351 else: |
|
352 return None |
|
353 |
|
354 def isModuleBeingImported(self, module): |
|
355 """ |
|
356 Public method to check for the given module is currently being |
|
357 imported. |
|
358 |
|
359 @param module module name to look for |
|
360 @type str |
|
361 @return flag indicating the given module was found |
|
362 @rtype bool |
|
363 """ |
|
364 return self.__context.get('module') == module |
|
365 |
|
366 def isModuleImportedExact(self, module): |
|
367 """ |
|
368 Public method to check if a given module has been imported; only exact |
|
369 matches. |
|
370 |
|
371 @param module module name to look for |
|
372 @type str |
|
373 @return flag indicating the given module was found |
|
374 @rtype bool |
|
375 """ |
|
376 return module in self.__context.get('imports', []) |
|
377 |
|
378 def isModuleImportedLike(self, module): |
|
379 """ |
|
380 Public method to check if a given module has been imported; given |
|
381 module exists. |
|
382 |
|
383 @param module module name to look for |
|
384 @type str |
|
385 @return flag indicating the given module was found |
|
386 @rtype bool |
|
387 """ |
|
388 if 'imports' in self.__context: |
|
389 for imp in self.__context['imports']: |
|
390 if module in imp: |
|
391 return True |
|
392 |
|
393 return False |