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