skylos 2.2.3__py3-none-any.whl → 2.4.0__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.

Potentially problematic release.


This version of skylos might be problematic. Click here for more details.

@@ -0,0 +1,208 @@
1
+ from __future__ import annotations
2
+ import ast
3
+ import sys
4
+
5
+ def _qualified_name_from_call(node):
6
+ func = node.func
7
+ parts = []
8
+ while isinstance(func, ast.Attribute):
9
+ parts.append(func.attr)
10
+ func = func.value
11
+ if isinstance(func, ast.Name):
12
+ parts.append(func.id)
13
+ parts.reverse()
14
+ return ".".join(parts)
15
+ if isinstance(func, ast.Name):
16
+ return func.id
17
+ return None
18
+
19
+ def _is_interpolated_string(node):
20
+ if isinstance(node, ast.JoinedStr):
21
+ return True
22
+
23
+ if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mod)):
24
+ return True
25
+
26
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "format":
27
+ return True
28
+ return False
29
+
30
+ def _add_finding(findings, file_path, node, rule_id, severity, message):
31
+ findings.append({
32
+ "rule_id": rule_id,
33
+ "severity": severity,
34
+ "message": message,
35
+ "file": str(file_path),
36
+ "line": getattr(node, "lineno", 1),
37
+ "col": getattr(node, "col_offset", 0),
38
+ })
39
+
40
+ class _CmdFlowChecker(ast.NodeVisitor):
41
+ OS_SYSTEM = "os.system"
42
+ SUBPROC_PREFIX = "subprocess."
43
+
44
+ def __init__(self, file_path, findings):
45
+ self.file_path = file_path
46
+ self.findings = findings
47
+ self.env_stack = [{}]
48
+ self.current_function = None
49
+
50
+ def _push(self):
51
+ self.env_stack.append({})
52
+
53
+ def _pop(self):
54
+ popped = self.env_stack.pop() # pragma: no skylos
55
+
56
+ def _set(self, name, tainted):
57
+ if not self.env_stack:
58
+ self._push()
59
+ self.env_stack[-1][name] = tainted
60
+
61
+ def _get(self, name):
62
+ for i, env in enumerate(reversed(self.env_stack)):
63
+ if name in env:
64
+ result = env[name]
65
+ return result
66
+ return False
67
+
68
+ def _tainted(self, node):
69
+
70
+ if _is_interpolated_string(node):
71
+ if isinstance(node, ast.JoinedStr):
72
+ for value in node.values:
73
+ if isinstance(value, ast.FormattedValue):
74
+ if self._tainted(value.value):
75
+ return True
76
+ return True
77
+
78
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "input":
79
+ return True
80
+
81
+ if isinstance(node, (ast.Attribute, ast.Subscript)):
82
+ base = node.value
83
+ while isinstance(base, ast.Attribute):
84
+ base = base.value
85
+ if isinstance(base, ast.Name) and base.id == "request":
86
+ return True
87
+
88
+ if isinstance(node, ast.Name):
89
+ result = self._get(node.id)
90
+ return result
91
+
92
+ if isinstance(node, (ast.Attribute, ast.Subscript)):
93
+ target_value = node.value
94
+ result = self._tainted(target_value)
95
+ return result
96
+
97
+ if isinstance(node, ast.BinOp):
98
+ left = self._tainted(node.left)
99
+ right = self._tainted(node.right)
100
+ result = left or right
101
+ return result
102
+
103
+ if isinstance(node, ast.Call):
104
+ for arg in node.args:
105
+ if self._tainted(arg):
106
+ return True
107
+ return False
108
+
109
+ return False
110
+
111
+ def _traverse_children(self, node):
112
+ for child in ast.iter_child_nodes(node):
113
+ self.visit(child)
114
+
115
+ def visit_FunctionDef(self, node):
116
+ self.current_function = node.name
117
+ self._push()
118
+
119
+ # for arg in node.args.args:
120
+ # self._set(arg.arg, True)
121
+
122
+ self._traverse_children(node)
123
+ self._pop()
124
+ self.current_function = None
125
+
126
+ def visit_AsyncFunctionDef(self, node):
127
+ self.current_function = node.name
128
+ self._push()
129
+
130
+ for arg in node.args.args:
131
+ self._set(arg.arg, True)
132
+
133
+ self._traverse_children(node)
134
+ self._pop()
135
+ self.current_function = None
136
+
137
+ def visit_Assign(self, node):
138
+ taint = self._tainted(node.value)
139
+ for tgt in node.targets:
140
+ if isinstance(tgt, ast.Name):
141
+ self._set(tgt.id, taint)
142
+ self._traverse_children(node)
143
+
144
+ def visit_AnnAssign(self, node):
145
+ taint = self._tainted(node.value) if node.value else False
146
+ if isinstance(node.target, ast.Name):
147
+ self._set(node.target.id, taint)
148
+ self._traverse_children(node)
149
+
150
+ def visit_AugAssign(self, node):
151
+ taint = self._tainted(node.target) or self._tainted(node.value)
152
+ if isinstance(node.target, ast.Name):
153
+ self._set(node.target.id, taint)
154
+ self._traverse_children(node)
155
+
156
+ def visit_Call(self, node):
157
+ qn = _qualified_name_from_call(node)
158
+
159
+ if qn == self.OS_SYSTEM and node.args:
160
+ arg0 = node.args[0]
161
+ is_interp = _is_interpolated_string(arg0)
162
+ is_taint = self._tainted(arg0)
163
+
164
+ if is_interp or is_taint:
165
+ _add_finding(
166
+ self.findings, self.file_path, node,
167
+ "SKY-D212", "CRITICAL",
168
+ "Possible command injection (RCE): string-built or tainted shell command."
169
+ )
170
+
171
+ if qn and qn.startswith(self.SUBPROC_PREFIX) and node.args:
172
+ shell_true = False
173
+ for kw in (node.keywords or []):
174
+ if kw.arg == "shell" and isinstance(kw.value, ast.Constant) and kw.value.value is True:
175
+ shell_true = True
176
+ break
177
+
178
+ if shell_true:
179
+ arg0 = node.args[0]
180
+ is_interp = _is_interpolated_string(arg0)
181
+ is_taint = self._tainted(arg0)
182
+
183
+ if is_interp or is_taint:
184
+ _add_finding(
185
+ self.findings, self.file_path, node,
186
+ "SKY-D212", "CRITICAL",
187
+ "Possible command injection (RCE): string-built or tainted command with shell=True."
188
+ )
189
+
190
+ self._traverse_children(node)
191
+
192
+ def generic_visit(self, node):
193
+ for field, value in ast.iter_fields(node):
194
+ if isinstance(value, list):
195
+ for item in value:
196
+ if isinstance(item, ast.AST):
197
+ self.visit(item)
198
+ elif isinstance(value, ast.AST):
199
+ self.visit(value)
200
+
201
+ def scan(tree, file_path, findings):
202
+ try:
203
+
204
+ checker = _CmdFlowChecker(file_path, findings)
205
+ checker.visit(tree)
206
+
207
+ except Exception as e:
208
+ print(f"CMD flow failed for {file_path}: {e}", file=sys.stderr)
File without changes
@@ -0,0 +1,188 @@
1
+ from __future__ import annotations
2
+ import ast
3
+ import sys
4
+
5
+ def _qualified_name_from_call(node):
6
+ func = node.func
7
+ parts = []
8
+ while isinstance(func, ast.Attribute):
9
+ parts.append(func.attr)
10
+ func = func.value
11
+
12
+ if isinstance(func, ast.Name):
13
+ parts.append(func.id)
14
+ parts.reverse()
15
+ return ".".join(parts)
16
+
17
+ if isinstance(func, ast.Name):
18
+ return func.id
19
+
20
+ return None
21
+
22
+ def _is_interpolated_string(node):
23
+ if isinstance(node, ast.JoinedStr):
24
+ return True
25
+
26
+ if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mod)):
27
+ return True
28
+
29
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "format":
30
+ return True
31
+
32
+ return False
33
+
34
+ def _add_finding(findings, file_path, node, rule_id, severity, message):
35
+ findings.append({
36
+ "rule_id": rule_id,
37
+ "severity": severity,
38
+ "message": message,
39
+ "file": str(file_path),
40
+ "line": getattr(node, "lineno", 1),
41
+ "col": getattr(node, "col_offset", 0),
42
+ })
43
+
44
+ class _PathFlowChecker(ast.NodeVisitor):
45
+
46
+ FILE_OPEN_FUNCS = {"open"}
47
+ OS_FILE_FUNCS = {"open", "unlink", "remove", "mkdir", "rmdir", "makedirs"}
48
+ SHUTIL_FUNCS = {"copy", "copy2", "copytree", "move", "rmtree"}
49
+
50
+ def __init__(self, file_path, findings):
51
+ self.file_path = file_path
52
+ self.findings = findings
53
+ self.env_stack = [{}]
54
+
55
+ def _push(self):
56
+ self.env_stack.append({})
57
+
58
+ def _pop(self):
59
+ self.env_stack.pop()
60
+
61
+ def _set(self, name, tainted):
62
+ if not self.env_stack:
63
+ self._push()
64
+ self.env_stack[-1][name] = bool(tainted)
65
+
66
+ def _get(self, name):
67
+ for env in reversed(self.env_stack):
68
+ if name in env:
69
+ return env[name]
70
+ return False
71
+
72
+ def _tainted(self, node):
73
+ if _is_interpolated_string(node):
74
+ return True
75
+
76
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "input":
77
+ return True
78
+
79
+ if isinstance(node, (ast.Attribute, ast.Subscript)):
80
+ base = node.value
81
+ while isinstance(base, ast.Attribute):
82
+ base = base.value
83
+ if isinstance(base, ast.Name) and base.id == "request":
84
+ return True
85
+
86
+ if isinstance(node, ast.Name):
87
+ value = self._get(node.id)
88
+ return value
89
+
90
+ if isinstance(node, (ast.Attribute, ast.Subscript)):
91
+ inner = node.value
92
+ result = self._tainted(inner)
93
+ return result
94
+
95
+ if isinstance(node, ast.BinOp):
96
+ left = self._tainted(node.left)
97
+ right = self._tainted(node.right)
98
+ if left:
99
+ return True
100
+ if right:
101
+ return True
102
+ return False
103
+
104
+ if isinstance(node, ast.Call):
105
+ for arg in node.args:
106
+ if self._tainted(arg):
107
+ return True
108
+ return False
109
+
110
+ return False
111
+
112
+ def _traverse_children(self, node):
113
+ for child in ast.iter_child_nodes(node):
114
+ self.visit(child)
115
+
116
+ def visit_FunctionDef(self, node):
117
+ self._push()
118
+ for arg in node.args.args:
119
+ self._set(arg.arg, True)
120
+ self._traverse_children(node)
121
+ self._pop()
122
+
123
+ def visit_AsyncFunctionDef(self, node):
124
+ self._push()
125
+ self._traverse_children(node)
126
+ self._pop()
127
+
128
+ def visit_Assign(self, node):
129
+ t = self._tainted(node.value)
130
+ for tgt in node.targets:
131
+ if isinstance(tgt, ast.Name):
132
+ self._set(tgt.id, t)
133
+ self._traverse_children(node)
134
+
135
+ def visit_AnnAssign(self, node):
136
+ if node.value is not None:
137
+ t = self._tainted(node.value)
138
+ else:
139
+ t = False
140
+ if isinstance(node.target, ast.Name):
141
+ self._set(node.target.id, t)
142
+ self._traverse_children(node)
143
+
144
+ def visit_AugAssign(self, node):
145
+ t = self._tainted(node.target) or self._tainted(node.value)
146
+ if isinstance(node.target, ast.Name):
147
+ self._set(node.target.id, t)
148
+ self._traverse_children(node)
149
+
150
+ def _flag_if_tainted_path(self, node, path_expr):
151
+ is_interp = _is_interpolated_string(path_expr)
152
+ is_tainted = self._tainted(path_expr)
153
+ if is_interp or is_tainted:
154
+ _add_finding(
155
+ self.findings, self.file_path, node,
156
+ "SKY-D215", "HIGH",
157
+ "Possible path traversal: tainted filesystem path"
158
+ )
159
+
160
+ def visit_Call(self, node):
161
+ qn = _qualified_name_from_call(node)
162
+
163
+ if qn and qn in self.FILE_OPEN_FUNCS and node.args:
164
+ self._flag_if_tainted_path(node, node.args[0])
165
+
166
+ if qn and "." in qn:
167
+ mod, func = qn.split(".", 1)
168
+ if mod == "os" and func in self.OS_FILE_FUNCS and node.args:
169
+ self._flag_if_tainted_path(node, node.args[0])
170
+
171
+ if mod == "shutil" and func in self.SHUTIL_FUNCS and node.args:
172
+ self._flag_if_tainted_path(node, node.args[0])
173
+
174
+ if func == "open" and node.args:
175
+ pass
176
+
177
+ self._traverse_children(node)
178
+
179
+ def generic_visit(self, node):
180
+ for child in ast.iter_child_nodes(node):
181
+ self.visit(child)
182
+
183
+ def scan(tree, file_path, findings):
184
+ try:
185
+ checker = _PathFlowChecker(file_path, findings)
186
+ checker.visit(tree)
187
+ except Exception as e:
188
+ print(f"Path traversal analysis failed for {file_path}: {e}", file=sys.stderr)
File without changes
@@ -0,0 +1,198 @@
1
+ from __future__ import annotations
2
+ import ast
3
+ import sys
4
+
5
+ """
6
+ url = input()
7
+ requests.get(url) # attacker controls destination.. can hit internal metadata services, etc.
8
+ """
9
+
10
+ def _qualified_name_from_call(node):
11
+ func = node.func
12
+ parts = []
13
+ while isinstance(func, ast.Attribute):
14
+ parts.append(func.attr)
15
+ func = func.value
16
+
17
+ if isinstance(func, ast.Name):
18
+ parts.append(func.id)
19
+ parts.reverse()
20
+ return ".".join(parts)
21
+
22
+ if isinstance(func, ast.Name):
23
+ return func.id
24
+
25
+ return None
26
+
27
+ def _is_interpolated_string(node):
28
+ if isinstance(node, ast.JoinedStr):
29
+ return True
30
+
31
+ if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Mod)):
32
+ return True
33
+
34
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) and node.func.attr == "format":
35
+ return True
36
+
37
+ return False
38
+
39
+ def _add_finding(findings, file_path, node, rule_id, severity, message):
40
+ findings.append({
41
+ "rule_id": rule_id,
42
+ "severity": severity,
43
+ "message": message,
44
+ "file": str(file_path),
45
+ "line": getattr(node, "lineno", 1),
46
+ "col": getattr(node, "col_offset", 0),
47
+ })
48
+
49
+ class _SSRFFlowChecker(ast.NodeVisitor):
50
+
51
+ HTTP_METHODS = {"get", "post", "put", "delete", "head", "options", "request"}
52
+
53
+ def __init__(self, file_path, findings):
54
+ self.file_path = file_path
55
+ self.findings = findings
56
+ self.env_stack = []
57
+
58
+ def _push(self):
59
+ self.env_stack.append({})
60
+
61
+ def _pop(self):
62
+ self.env_stack.pop()
63
+
64
+ def _set(self, name, tainted):
65
+ if not self.env_stack:
66
+ self._push()
67
+ self.env_stack[-1][name] = bool(tainted)
68
+
69
+ def _get(self, name):
70
+ for env in reversed(self.env_stack):
71
+ if name in env:
72
+ return env[name]
73
+ return False
74
+
75
+ def _tainted(self, node):
76
+ if _is_interpolated_string(node):
77
+ return True
78
+
79
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "input":
80
+ return True
81
+
82
+ if isinstance(node, ast.Call):
83
+ qn = _qualified_name_from_call(node)
84
+ if qn and qn.startswith("request."):
85
+ return True
86
+
87
+ if isinstance(node, (ast.Attribute, ast.Subscript)):
88
+ base = node.value
89
+ while isinstance(base, ast.Attribute):
90
+ base = base.value
91
+ if isinstance(base, ast.Name) and base.id == "request":
92
+ return True
93
+
94
+ if isinstance(node, ast.Name):
95
+ value = self._get(node.id)
96
+ return value
97
+
98
+ if isinstance(node, (ast.Attribute, ast.Subscript)):
99
+ inner = node.value
100
+ result = self._tainted(inner)
101
+ return result
102
+
103
+ if isinstance(node, ast.BinOp):
104
+ left = self._tainted(node.left)
105
+ right = self._tainted(node.right)
106
+ if left:
107
+ return True
108
+ if right:
109
+ return True
110
+ return False
111
+
112
+ if isinstance(node, ast.Call):
113
+ for arg in node.args:
114
+ if self._tainted(arg):
115
+ return True
116
+ return False
117
+
118
+ return False
119
+
120
+ def visit_FunctionDef(self, node):
121
+ self._push()
122
+ self.generic_visit(node)
123
+ self._pop()
124
+
125
+ def visit_AsyncFunctionDef(self, node):
126
+ self._push()
127
+ self.generic_visit(node)
128
+ self._pop()
129
+
130
+ def visit_Assign(self, node):
131
+ taint = self._tainted(node.value)
132
+ for tgt in node.targets:
133
+ if isinstance(tgt, ast.Name):
134
+ self._set(tgt.id, taint)
135
+ self.generic_visit(node)
136
+
137
+ def visit_AnnAssign(self, node):
138
+ if node.value is not None:
139
+ taint = self._tainted(node.value)
140
+ else:
141
+ taint = False
142
+
143
+ if isinstance(node.target, ast.Name):
144
+ self._set(node.target.id, taint)
145
+ self.generic_visit(node)
146
+
147
+ def visit_AugAssign(self, node):
148
+ taint = self._tainted(node.target) or self._tainted(node.value)
149
+ if isinstance(node.target, ast.Name):
150
+ self._set(node.target.id, taint)
151
+ self.generic_visit(node)
152
+
153
+ def visit_Call(self, node):
154
+ qn = _qualified_name_from_call(node)
155
+
156
+ if qn and "." in qn:
157
+ _, func = qn.split(".", 1)
158
+ if func in self.HTTP_METHODS and node.args:
159
+ url_arg = node.args[0]
160
+ is_interp = _is_interpolated_string(url_arg)
161
+ is_tainted = self._tainted(url_arg)
162
+
163
+ if is_interp or is_tainted:
164
+ _add_finding(
165
+ self.findings, self.file_path, node,
166
+ "SKY-D216", "CRITICAL",
167
+ "Possible SSRF: tainted URL passed to HTTP client."
168
+ )
169
+
170
+ if qn and qn.endswith(".urlopen") and node.args:
171
+ url_arg = node.args[0]
172
+ is_interp = _is_interpolated_string(url_arg)
173
+ is_tainted = self._tainted(url_arg)
174
+
175
+ if is_interp or is_tainted:
176
+ _add_finding(
177
+ self.findings, self.file_path, node,
178
+ "SKY-D216", "CRITICAL",
179
+ "Possible SSRF: tainted URL passed to HTTP client."
180
+ )
181
+
182
+ self.generic_visit(node)
183
+
184
+ def generic_visit(self, node):
185
+ for field, value in ast.iter_fields(node):
186
+ if isinstance(value, list):
187
+ for item in value:
188
+ if isinstance(item, ast.AST):
189
+ self.visit(item)
190
+ elif isinstance(value, ast.AST):
191
+ self.visit(value)
192
+
193
+ def scan(tree, file_path, findings):
194
+ try:
195
+ checker = _SSRFFlowChecker(file_path, findings)
196
+ checker.visit(tree)
197
+ except Exception as e:
198
+ print(f"SSRF flow analysis failed for {file_path}: {e}", file=sys.stderr)
File without changes