skylos 1.0.10__py3-none-any.whl → 2.5.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. skylos/__init__.py +9 -3
  2. skylos/analyzer.py +674 -168
  3. skylos/cfg_visitor.py +60 -0
  4. skylos/cli.py +719 -235
  5. skylos/codemods.py +277 -0
  6. skylos/config.py +50 -0
  7. skylos/constants.py +78 -0
  8. skylos/gatekeeper.py +147 -0
  9. skylos/linter.py +18 -0
  10. skylos/rules/base.py +20 -0
  11. skylos/rules/danger/calls.py +119 -0
  12. skylos/rules/danger/danger.py +157 -0
  13. skylos/rules/danger/danger_cmd/cmd_flow.py +75 -0
  14. skylos/rules/danger/danger_fs/__init__.py +0 -0
  15. skylos/rules/danger/danger_fs/path_flow.py +79 -0
  16. skylos/rules/danger/danger_net/__init__.py +0 -0
  17. skylos/rules/danger/danger_net/ssrf_flow.py +80 -0
  18. skylos/rules/danger/danger_sql/__init__.py +0 -0
  19. skylos/rules/danger/danger_sql/sql_flow.py +245 -0
  20. skylos/rules/danger/danger_sql/sql_raw_flow.py +96 -0
  21. skylos/rules/danger/danger_web/__init__.py +0 -0
  22. skylos/rules/danger/danger_web/xss_flow.py +170 -0
  23. skylos/rules/danger/taint.py +110 -0
  24. skylos/rules/quality/__init__.py +0 -0
  25. skylos/rules/quality/complexity.py +95 -0
  26. skylos/rules/quality/logic.py +96 -0
  27. skylos/rules/quality/nesting.py +101 -0
  28. skylos/rules/quality/structure.py +99 -0
  29. skylos/rules/secrets.py +325 -0
  30. skylos/server.py +554 -0
  31. skylos/visitor.py +502 -90
  32. skylos/visitors/__init__.py +0 -0
  33. skylos/visitors/framework_aware.py +437 -0
  34. skylos/visitors/test_aware.py +74 -0
  35. skylos-2.5.2.dist-info/METADATA +21 -0
  36. skylos-2.5.2.dist-info/RECORD +42 -0
  37. {skylos-1.0.10.dist-info → skylos-2.5.2.dist-info}/WHEEL +1 -1
  38. {skylos-1.0.10.dist-info → skylos-2.5.2.dist-info}/top_level.txt +0 -1
  39. skylos-1.0.10.dist-info/METADATA +0 -8
  40. skylos-1.0.10.dist-info/RECORD +0 -21
  41. test/compare_tools.py +0 -604
  42. test/diagnostics.py +0 -364
  43. test/sample_repo/app.py +0 -13
  44. test/sample_repo/sample_repo/commands.py +0 -81
  45. test/sample_repo/sample_repo/models.py +0 -122
  46. test/sample_repo/sample_repo/routes.py +0 -89
  47. test/sample_repo/sample_repo/utils.py +0 -36
  48. test/test_skylos.py +0 -456
  49. test/test_visitor.py +0 -220
  50. {test → skylos/rules}/__init__.py +0 -0
  51. {test/sample_repo → skylos/rules/danger}/__init__.py +0 -0
  52. {test/sample_repo/sample_repo → skylos/rules/danger/danger_cmd}/__init__.py +0 -0
  53. {skylos-1.0.10.dist-info → skylos-2.5.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,245 @@
1
+ from __future__ import annotations
2
+ import ast
3
+ import sys
4
+ from skylos.rules.danger.taint import TaintVisitor
5
+
6
+
7
+ def _qualified_name_from_call(node):
8
+ func = node.func
9
+ parts = []
10
+
11
+ while isinstance(func, ast.Attribute):
12
+ parts.append(func.attr)
13
+ func = func.value
14
+ if isinstance(func, ast.Name):
15
+ parts.append(func.id)
16
+ parts.reverse()
17
+ return ".".join(parts)
18
+ return None
19
+
20
+
21
+ def _is_interpolated_string(node):
22
+ if isinstance(node, ast.JoinedStr):
23
+ return True
24
+ if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mod)):
25
+ return True
26
+ if (
27
+ isinstance(node, ast.Call)
28
+ and isinstance(node.func, ast.Attribute)
29
+ and node.func.attr == "format"
30
+ ):
31
+ return True
32
+ return False
33
+
34
+
35
+ def _is_passthrough_return(node: ast.AST, param_names):
36
+ if isinstance(node, ast.Name) and node.id in param_names:
37
+ return True
38
+
39
+ if isinstance(node, ast.JoinedStr):
40
+ for v in node.values:
41
+ if (
42
+ isinstance(v, ast.FormattedValue)
43
+ and isinstance(v.value, ast.Name)
44
+ and v.value.id in param_names
45
+ ):
46
+ return True
47
+ return True
48
+
49
+ if (
50
+ isinstance(node, ast.Call)
51
+ and isinstance(node.func, ast.Attribute)
52
+ and node.func.attr == "format"
53
+ ):
54
+ return True
55
+ if isinstance(node, ast.BinOp):
56
+ return True
57
+ return False
58
+
59
+
60
+ def _func_name(node):
61
+ return node.name
62
+
63
+
64
+ def get_query_expression(call: ast.Call, names=("sql", "query", "statement")):
65
+ expression = None
66
+ if call.args and len(call.args) > 0:
67
+ expression = call.args[0]
68
+ if expression is None:
69
+ for keyword in call.keywords or []:
70
+ if keyword.arg in names and keyword.value is not None:
71
+ expression = keyword.value
72
+ break
73
+ return expression
74
+
75
+
76
+ def is_parameterized_query(call: ast.Call, query_expr: ast.AST):
77
+ if _is_interpolated_string(query_expr):
78
+ return False
79
+
80
+ if len(call.args) >= 2:
81
+ return True
82
+
83
+ for keyword in call.keywords or []:
84
+ if keyword.arg in {"params", "parameters"}:
85
+ return True
86
+ return False
87
+
88
+
89
+ def is_sqlalchemy_text(expr: ast.AST):
90
+ if not isinstance(expr, ast.Call):
91
+ return False
92
+
93
+ func = expr.func
94
+
95
+ if isinstance(func, ast.Attribute) and func.attr == "text":
96
+ return True
97
+
98
+ if isinstance(func, ast.Name) and func.id == "text":
99
+ return True
100
+ return False
101
+
102
+
103
+ class _SQLFlowChecker(TaintVisitor):
104
+ RULE_ID_SQLI = "SKY-D211"
105
+ SEVERITY_CRITICAL = "CRITICAL"
106
+ SEVERITY_HIGH = "HIGH"
107
+ DBAPI_SQL_SINK_SUFFIXES = (".execute", ".executemany", ".executescript")
108
+
109
+ def __init__(self, file_path, findings):
110
+ super().__init__(file_path, findings)
111
+ self.passthrough_functions = set()
112
+
113
+ def visit_FunctionDef(self, node):
114
+ self._push()
115
+
116
+ param_names = {a.arg for a in node.args.args}
117
+ for statement in node.body:
118
+ if isinstance(statement, ast.Return) and statement.value is not None:
119
+ if _is_passthrough_return(statement.value, param_names):
120
+ self.passthrough_functions.add(_func_name(node))
121
+ break
122
+
123
+ self.generic_visit(node)
124
+ self._pop()
125
+
126
+ def visit_AsyncFunctionDef(self, node):
127
+ self._push()
128
+
129
+ param_names = {a.arg for a in node.args.args}
130
+ for statement in node.body:
131
+ if isinstance(statement, ast.Return) and statement.value is not None:
132
+ if _is_passthrough_return(statement.value, param_names):
133
+ self.passthrough_functions.add(_func_name(node))
134
+ break
135
+
136
+ self.generic_visit(node)
137
+ self._pop()
138
+
139
+ def visit_Call(self, node):
140
+ qual_name = _qualified_name_from_call(node)
141
+
142
+ if qual_name and qual_name in self.passthrough_functions:
143
+ pass
144
+
145
+ if qual_name and qual_name.endswith(self.DBAPI_SQL_SINK_SUFFIXES):
146
+ query_expr = get_query_expression(node, names=("sql", "query", "statement"))
147
+
148
+ if query_expr is not None:
149
+ if _is_interpolated_string(query_expr) or self.is_tainted(query_expr):
150
+ self.findings.append(
151
+ {
152
+ "rule_id": self.RULE_ID_SQLI,
153
+ "severity": self.SEVERITY_CRITICAL,
154
+ "message": "Possible SQL injection: tainted or string-built query.",
155
+ "file": str(self.file_path),
156
+ "line": node.lineno,
157
+ "col": node.col_offset,
158
+ }
159
+ )
160
+ else:
161
+ is_literal = isinstance(query_expr, ast.Constant) and isinstance(
162
+ query_expr.value, str
163
+ )
164
+ if not is_literal and not is_parameterized_query(node, query_expr):
165
+ self.findings.append(
166
+ {
167
+ "rule_id": self.RULE_ID_SQLI,
168
+ "severity": self.SEVERITY_HIGH,
169
+ "message": "Likely unparameterized SQL execution.",
170
+ "file": str(self.file_path),
171
+ "line": node.lineno,
172
+ "col": node.col_offset,
173
+ }
174
+ )
175
+
176
+ self.generic_visit(node)
177
+ return
178
+
179
+ if qual_name and (
180
+ qual_name.endswith(".read_sql") or qual_name.endswith(".read_sql_query")
181
+ ):
182
+ query_expr = get_query_expression(node, names=("sql", "query"))
183
+
184
+ if query_expr is not None and (
185
+ _is_interpolated_string(query_expr) or self.is_tainted(query_expr)
186
+ ):
187
+ self.findings.append(
188
+ {
189
+ "rule_id": self.RULE_ID_SQLI,
190
+ "severity": self.SEVERITY_CRITICAL,
191
+ "message": "Possible SQL injection in read_sql.",
192
+ "file": str(self.file_path),
193
+ "line": node.lineno,
194
+ "col": node.col_offset,
195
+ }
196
+ )
197
+ self.generic_visit(node)
198
+ return
199
+
200
+ if isinstance(node.func, ast.Attribute) and node.func.attr == "execute":
201
+ statement_expression = get_query_expression(
202
+ node, names=("statement", "sql", "query")
203
+ )
204
+ if statement_expression is not None:
205
+ if _is_interpolated_string(statement_expression) or self.is_tainted(
206
+ statement_expression
207
+ ):
208
+ self.findings.append(
209
+ {
210
+ "rule_id": self.RULE_ID_SQLI,
211
+ "severity": self.SEVERITY_CRITICAL,
212
+ "message": "Possible SQL injection: tainted statement passed to execute().",
213
+ "file": str(self.file_path),
214
+ "line": node.lineno,
215
+ "col": node.col_offset,
216
+ }
217
+ )
218
+
219
+ self.generic_visit(node)
220
+ return
221
+
222
+ if is_sqlalchemy_text(node):
223
+ for argument in node.args:
224
+ if _is_interpolated_string(argument) or self.is_tainted(argument):
225
+ self.findings.append(
226
+ {
227
+ "rule_id": self.RULE_ID_SQLI,
228
+ "severity": self.SEVERITY_CRITICAL,
229
+ "message": "Possible SQL injection: tainted string used in sqlalchemy.text().",
230
+ "file": str(self.file_path),
231
+ "line": node.lineno,
232
+ "col": node.col_offset,
233
+ }
234
+ )
235
+ break
236
+
237
+ self.generic_visit(node)
238
+
239
+
240
+ def scan(tree, file_path, findings):
241
+ try:
242
+ checker = _SQLFlowChecker(file_path, findings)
243
+ checker.visit(tree)
244
+ except Exception as e:
245
+ print(f"SQL flow analysis failed for {file_path}: {e}", file=sys.stderr)
@@ -0,0 +1,96 @@
1
+ from __future__ import annotations
2
+ import ast
3
+ import sys
4
+ from skylos.rules.danger.taint import TaintVisitor
5
+
6
+
7
+ def _qualified_name(node: ast.Call):
8
+ f = node.func
9
+ parts = []
10
+
11
+ while isinstance(f, ast.Attribute):
12
+ parts.append(f.attr)
13
+ f = f.value
14
+
15
+ if isinstance(f, ast.Name):
16
+ parts.append(f.id)
17
+ parts.reverse()
18
+ return ".".join(parts)
19
+
20
+ if isinstance(f, ast.Name):
21
+ return f.id
22
+ return None
23
+
24
+
25
+ def _is_interpolated_string(n: ast.AST):
26
+ if isinstance(n, ast.JoinedStr):
27
+ return True
28
+ if isinstance(n, ast.BinOp) and isinstance(n.op, (ast.Add, ast.Mod)):
29
+ return True
30
+ if (
31
+ isinstance(n, ast.Call)
32
+ and isinstance(n.func, ast.Attribute)
33
+ and n.func.attr == "format"
34
+ ):
35
+ return True
36
+ return False
37
+
38
+
39
+ class _SQLRawFlowChecker(TaintVisitor):
40
+ def visit_Call(self, node: ast.Call):
41
+ qn = _qualified_name(node)
42
+ if not qn:
43
+ self.generic_visit(node)
44
+ return
45
+
46
+ if qn.endswith(".text") and node.args:
47
+ sql = node.args[0]
48
+ if _is_interpolated_string(sql) or self.is_tainted(sql):
49
+ self.findings.append(
50
+ {
51
+ "rule_id": "SKY-D217",
52
+ "severity": "CRITICAL",
53
+ "message": "Possible SQL injection: tainted SQL passed to sqlalchemy.text().",
54
+ "file": str(self.file_path),
55
+ "line": node.lineno,
56
+ "col": node.col_offset,
57
+ }
58
+ )
59
+
60
+ if (qn.endswith(".read_sql") or qn.endswith(".read_sql_query")) and node.args:
61
+ sql = node.args[0]
62
+ if _is_interpolated_string(sql) or self.is_tainted(sql):
63
+ self.findings.append(
64
+ {
65
+ "rule_id": "SKY-D217",
66
+ "severity": "CRITICAL",
67
+ "message": "Possible SQL injection: tainted SQL passed to pandas.read_sql().",
68
+ "file": str(self.file_path),
69
+ "line": node.lineno,
70
+ "col": node.col_offset,
71
+ }
72
+ )
73
+
74
+ if qn.endswith(".objects.raw") and node.args:
75
+ sql = node.args[0]
76
+ if _is_interpolated_string(sql) or self.is_tainted(sql):
77
+ self.findings.append(
78
+ {
79
+ "rule_id": "SKY-D217",
80
+ "severity": "CRITICAL",
81
+ "message": "Possible SQL injection: tainted SQL passed to Django .raw().",
82
+ "file": str(self.file_path),
83
+ "line": node.lineno,
84
+ "col": node.col_offset,
85
+ }
86
+ )
87
+
88
+ self.generic_visit(node)
89
+
90
+
91
+ def scan(tree: ast.AST, file_path, findings):
92
+ try:
93
+ checker = _SQLRawFlowChecker(file_path, findings)
94
+ checker.visit(tree)
95
+ except Exception as e:
96
+ print(f"Raw SQL flow analysis failed for {file_path}: {e}", file=sys.stderr)
File without changes
@@ -0,0 +1,170 @@
1
+ from __future__ import annotations
2
+ import ast
3
+ import sys
4
+ from skylos.rules.danger.taint import TaintVisitor
5
+
6
+
7
+ def _qualified_name_from_call(node: ast.Call):
8
+ func = node.func
9
+ parts = []
10
+ while isinstance(func, ast.Attribute):
11
+ parts.append(func.attr)
12
+ func = func.value
13
+ if isinstance(func, ast.Name):
14
+ parts.append(func.id)
15
+ parts.reverse()
16
+ return ".".join(parts)
17
+ if isinstance(func, ast.Name):
18
+ return func.id
19
+ return None
20
+
21
+
22
+ def _is_interpolated_string(node: ast.AST):
23
+ if isinstance(node, ast.JoinedStr):
24
+ return True
25
+ if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mod)):
26
+ return True
27
+ if (
28
+ isinstance(node, ast.Call)
29
+ and isinstance(node.func, ast.Attribute)
30
+ and node.func.attr == "format"
31
+ ):
32
+ return True
33
+ return False
34
+
35
+
36
+ def _const_str_value(node: ast.AST):
37
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
38
+ return node.value
39
+ return None
40
+
41
+
42
+ def _const_contains_html(node: ast.AST):
43
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
44
+ s = node.value
45
+ return ("<" in s) and (">" in s)
46
+ return False
47
+
48
+
49
+ class _XSSFlowChecker(TaintVisitor):
50
+ SAFE_MARK_FUNCS = {"Markup", "mark_safe"}
51
+
52
+ def _template_is_unsafe_literal(self, node: ast.AST):
53
+ s = _const_str_value(node)
54
+ if not s:
55
+ return False
56
+ low = s.lower()
57
+ if "|safe" in low:
58
+ return True
59
+ if "{% autoescape false %}" in low:
60
+ return True
61
+ return False
62
+
63
+ def _binop_has_html_const(self, node: ast.AST):
64
+ if _const_contains_html(node):
65
+ return True
66
+ if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mod)):
67
+ return self._binop_has_html_const(node.left) or self._binop_has_html_const(
68
+ node.right
69
+ )
70
+ return False
71
+
72
+ def _html_built_with_taint(self, node: ast.AST):
73
+ if isinstance(node, ast.JoinedStr):
74
+ has_html = False
75
+ for v in node.values:
76
+ if isinstance(v, ast.Constant) and _const_contains_html(v):
77
+ has_html = True
78
+ break
79
+ if not has_html:
80
+ return False
81
+
82
+ for v in node.values:
83
+ if isinstance(v, ast.FormattedValue) and self.is_tainted(v.value):
84
+ return True
85
+ return False
86
+
87
+ if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mod)):
88
+ left_html = _const_contains_html(node.left)
89
+ right_html = _const_contains_html(node.right)
90
+ any_html = (
91
+ left_html
92
+ or right_html
93
+ or self._binop_has_html_const(node.left)
94
+ or self._binop_has_html_const(node.right)
95
+ )
96
+
97
+ if not any_html:
98
+ return False
99
+ return self.is_tainted(node.left) or self.is_tainted(node.right)
100
+
101
+ if (
102
+ isinstance(node, ast.Call)
103
+ and isinstance(node.func, ast.Attribute)
104
+ and node.func.attr == "format"
105
+ ):
106
+ base = node.func.value
107
+ if _const_contains_html(base):
108
+ for a in node.args:
109
+ if self.is_tainted(a):
110
+ return True
111
+ return False
112
+ return False
113
+
114
+ def visit_Call(self, node: ast.Call):
115
+ qn = _qualified_name_from_call(node)
116
+
117
+ if qn and node.args:
118
+ func_name = qn.split(".")[-1]
119
+ if func_name in self.SAFE_MARK_FUNCS:
120
+ arg0 = node.args[0]
121
+ if _is_interpolated_string(arg0) or self.is_tainted(arg0):
122
+ self.findings.append(
123
+ {
124
+ "rule_id": "SKY-D226",
125
+ "severity": "CRITICAL",
126
+ "message": "Possible XSS: untrusted content marked safe",
127
+ "file": str(self.file_path),
128
+ "line": node.lineno,
129
+ "col": node.col_offset,
130
+ }
131
+ )
132
+
133
+ if qn and qn.split(".")[-1] == "render_template_string" and node.args:
134
+ tmpl = node.args[0]
135
+ if self._template_is_unsafe_literal(tmpl):
136
+ self.findings.append(
137
+ {
138
+ "rule_id": "SKY-D227",
139
+ "severity": "HIGH",
140
+ "message": "Possible XSS: unsafe inline template disables escaping",
141
+ "file": str(self.file_path),
142
+ "line": node.lineno,
143
+ "col": node.col_offset,
144
+ }
145
+ )
146
+
147
+ self.generic_visit(node)
148
+
149
+ def visit_Return(self, node: ast.Return):
150
+ if node.value is not None:
151
+ if self._html_built_with_taint(node.value):
152
+ self.findings.append(
153
+ {
154
+ "rule_id": "SKY-D228",
155
+ "severity": "HIGH",
156
+ "message": "XSS (HTML built from unescaped user input)",
157
+ "file": str(self.file_path),
158
+ "line": node.lineno,
159
+ "col": node.col_offset,
160
+ }
161
+ )
162
+ self.generic_visit(node)
163
+
164
+
165
+ def scan(tree, file_path, findings):
166
+ try:
167
+ checker = _XSSFlowChecker(file_path, findings)
168
+ checker.visit(tree)
169
+ except Exception as e:
170
+ print(f"XSS analysis failed for {file_path}: {e}", file=sys.stderr)
@@ -0,0 +1,110 @@
1
+ import ast
2
+
3
+
4
+ class TaintVisitor(ast.NodeVisitor):
5
+ def __init__(self, file_path, findings):
6
+ self.file_path = file_path
7
+ self.findings = findings
8
+ self.env_stack = [{}]
9
+ self.sources = {"input", "request"}
10
+ self.request_obj = "request"
11
+
12
+ def _push(self):
13
+ self.env_stack.append({})
14
+
15
+ def _pop(self):
16
+ if self.env_stack:
17
+ self.env_stack.pop()
18
+
19
+ def _set(self, name, tainted):
20
+ if not self.env_stack:
21
+ self._push()
22
+ self.env_stack[-1][name] = bool(tainted)
23
+
24
+ def _get(self, name):
25
+ for env in reversed(self.env_stack):
26
+ if name in env:
27
+ return env[name]
28
+ return False
29
+
30
+ def is_tainted(self, node):
31
+ if node is None:
32
+ return False
33
+
34
+ if isinstance(node, ast.JoinedStr):
35
+ return any(
36
+ isinstance(v, ast.FormattedValue) and self.is_tainted(v.value)
37
+ for v in node.values
38
+ )
39
+ if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mod)):
40
+ return self.is_tainted(node.left) or self.is_tainted(node.right)
41
+ if (
42
+ isinstance(node, ast.Call)
43
+ and isinstance(node.func, ast.Attribute)
44
+ and node.func.attr == "format"
45
+ ):
46
+ return True
47
+
48
+ if (
49
+ isinstance(node, ast.Call)
50
+ and isinstance(node.func, ast.Name)
51
+ and node.func.id in self.sources
52
+ ):
53
+ return True
54
+
55
+ if isinstance(node, (ast.Attribute, ast.Subscript)):
56
+ base = node.value
57
+ while isinstance(base, (ast.Attribute, ast.Subscript)):
58
+ base = base.value
59
+ if isinstance(base, ast.Name) and base.id == self.request_obj:
60
+ return True
61
+ return self.is_tainted(node.value)
62
+
63
+ if isinstance(node, ast.Name):
64
+ return self._get(node.id)
65
+
66
+ if isinstance(node, ast.Call):
67
+ if isinstance(node.func, ast.Attribute):
68
+ if self.is_tainted(node.func.value):
69
+ return True
70
+
71
+ if any(self.is_tainted(arg) for arg in node.args):
72
+ return True
73
+
74
+ if any(self.is_tainted(k.value) for k in node.keywords):
75
+ return True
76
+
77
+ return False
78
+
79
+ def generic_visit(self, node):
80
+ for field, value in ast.iter_fields(node):
81
+ if isinstance(value, list):
82
+ for item in value:
83
+ if isinstance(item, ast.AST):
84
+ self.visit(item)
85
+ elif isinstance(value, ast.AST):
86
+ self.visit(value)
87
+
88
+ def visit_FunctionDef(self, node):
89
+ self._push()
90
+ self.generic_visit(node)
91
+ self._pop()
92
+
93
+ def visit_AsyncFunctionDef(self, node):
94
+ self._push()
95
+ self.generic_visit(node)
96
+ self._pop()
97
+
98
+ def visit_Assign(self, node):
99
+ t = self.is_tainted(node.value)
100
+ for tgt in node.targets:
101
+ if isinstance(tgt, ast.Name):
102
+ self._set(tgt.id, t)
103
+ self.generic_visit(node)
104
+
105
+ def visit_AnnAssign(self, node):
106
+ if node.value:
107
+ t = self.is_tainted(node.value)
108
+ if isinstance(node.target, ast.Name):
109
+ self._set(node.target.id, t)
110
+ self.generic_visit(node)
File without changes