|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2020 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing an AST node visitor for security checks. |
|
8 """ |
|
9 |
|
10 import ast |
|
11 |
|
12 from . import SecurityUtils |
|
13 from .SecurityContext import SecurityContext |
|
14 |
|
15 |
|
16 class SecurityNodeVisitor(object): |
|
17 """ |
|
18 Class implementing an AST node visitor for security checks. |
|
19 """ |
|
20 def __init__(self, checker, secCheckers, filename): |
|
21 self.__checker = checker |
|
22 self.__securityCheckers = secCheckers |
|
23 |
|
24 self.seen = 0 |
|
25 self.depth = 0 |
|
26 self.filename = filename |
|
27 self.imports = set() |
|
28 self.import_aliases = {} |
|
29 |
|
30 # in some cases we can't determine a qualified name |
|
31 try: |
|
32 self.namespace = SecurityUtils.getModuleQualnameFromPath(filename) |
|
33 except SecurityUtils.InvalidModulePath: |
|
34 self.namespace = "" |
|
35 |
|
36 def __runChecks(self, checkType): |
|
37 """ |
|
38 Private method to run all enabled checks for a given check type. |
|
39 """ |
|
40 if checkType in self.__securityCheckers: |
|
41 for check in self.__securityCheckers[checkType]: |
|
42 check(self.__checker.reportError, |
|
43 SecurityContext(self.__context), |
|
44 self.__checker.getConfig()) |
|
45 |
|
46 def visit_ClassDef(self, node): |
|
47 """ |
|
48 Public method defining a visitor for AST ClassDef nodes. |
|
49 |
|
50 Add class name to current namespace for all descendants. |
|
51 |
|
52 @param node reference to the node being inspected |
|
53 @type ast.ClassDef |
|
54 """ |
|
55 # For all child nodes, add this class name to current namespace |
|
56 self.namespace = SecurityUtils.namespacePathJoin( |
|
57 self.namespace, node.name) |
|
58 |
|
59 def visit_FunctionDef(self, node): |
|
60 """ |
|
61 Public method defining a visitor for AST FunctionDef nodes. |
|
62 |
|
63 Add relevant information about the node to the context for use in tests |
|
64 which inspect function definitions. Add the function name to the |
|
65 current namespace for all descendants. |
|
66 |
|
67 @param node reference to the node being inspected |
|
68 @type ast.FunctionDef |
|
69 """ |
|
70 self.__context['function'] = node |
|
71 qualname = SecurityUtils.namespacePathJoin(self.namespace, node.name) |
|
72 name = qualname.split('.')[-1] |
|
73 self.__context['qualname'] = qualname |
|
74 self.__context['name'] = name |
|
75 |
|
76 # For all child nodes and any tests run, add this function name to |
|
77 # current namespace |
|
78 self.namespace = SecurityUtils.namespacePathJoin( |
|
79 self.namespace, node.name) |
|
80 |
|
81 self.__runChecks("FunctionDef") |
|
82 |
|
83 def visit_Call(self, node): |
|
84 """ |
|
85 Public method defining a visitor for AST Call nodes. |
|
86 |
|
87 Add relevant information about the node to the context for use in tests |
|
88 which inspect function calls. |
|
89 |
|
90 @param node reference to the node being inspected |
|
91 @type ast.Call |
|
92 """ |
|
93 self.__context['call'] = node |
|
94 qualname = SecurityUtils.getCallName(node, self.import_aliases) |
|
95 name = qualname.split('.')[-1] |
|
96 self.__context['qualname'] = qualname |
|
97 self.__context['name'] = name |
|
98 self.__runChecks("Call") |
|
99 |
|
100 def __preVisit(self, node): |
|
101 """ |
|
102 Private method to set up a context for the visit method. |
|
103 |
|
104 @param node node to base the context on |
|
105 @type ast.AST |
|
106 """ |
|
107 self.__context = {} |
|
108 self.__context['imports'] = self.imports |
|
109 self.__context['import_aliases'] = self.import_aliases |
|
110 |
|
111 if hasattr(node, 'lineno'): |
|
112 self.__context['lineno'] = node.lineno |
|
113 ## |
|
114 ## if node.lineno in self.nosec_lines: |
|
115 ## LOG.debug("skipped, nosec") |
|
116 ## self.metrics.note_nosec() |
|
117 ## return False |
|
118 |
|
119 self.__context['node'] = node |
|
120 self.__context['linerange'] = SecurityUtils.linerange_fix(node) |
|
121 self.__context['filename'] = self.filename |
|
122 |
|
123 self.seen += 1 |
|
124 self.depth += 1 |
|
125 |
|
126 return True |
|
127 |
|
128 def visit(self, node): |
|
129 """ |
|
130 Public method to inspected an AST node. |
|
131 |
|
132 @param node AST node to be inspected |
|
133 @type ast.AST |
|
134 """ |
|
135 name = node.__class__.__name__ |
|
136 method = 'visit_' + name |
|
137 visitor = getattr(self, method, None) |
|
138 if visitor is not None: |
|
139 visitor(node) |
|
140 else: |
|
141 self.__runChecks(name) |
|
142 |
|
143 def __postVisit(self, node): |
|
144 """ |
|
145 Private method to clean up after a node was visited. |
|
146 |
|
147 @param node AST node that was visited |
|
148 @type ast.AST |
|
149 """ |
|
150 self.depth -= 1 |
|
151 # Clean up post-recursion stuff that gets setup in the visit methods |
|
152 # for these node types. |
|
153 if isinstance(node, (ast.FunctionDef, ast.ClassDef)): |
|
154 self.namespace = SecurityUtils.namespacePathSplit( |
|
155 self.namespace)[0] |
|
156 |
|
157 def generic_visit(self, node): |
|
158 """ |
|
159 Public method to drive the node visitor. |
|
160 |
|
161 @param node node to be inspected |
|
162 @type ast.AST |
|
163 """ |
|
164 for _, value in ast.iter_fields(node): |
|
165 if isinstance(value, list): |
|
166 maxIndex = len(value) - 1 |
|
167 for index, item in enumerate(value): |
|
168 if isinstance(item, ast.AST): |
|
169 if index < maxIndex: |
|
170 item._securitySibling = value[index + 1] |
|
171 else: |
|
172 item._securitySibling = None |
|
173 item._securityParent = node |
|
174 |
|
175 if self.__preVisit(item): |
|
176 self.visit(item) |
|
177 self.generic_visit(item) |
|
178 self.__postVisit(item) |
|
179 |
|
180 elif isinstance(value, ast.AST): |
|
181 value._securitySibling = None |
|
182 value._securityParent = node |
|
183 if self.__preVisit(value): |
|
184 self.visit(value) |
|
185 self.generic_visit(value) |
|
186 self.__postVisit(value) |