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.
- skylos/__init__.py +9 -3
- skylos/analyzer.py +674 -168
- skylos/cfg_visitor.py +60 -0
- skylos/cli.py +719 -235
- skylos/codemods.py +277 -0
- skylos/config.py +50 -0
- skylos/constants.py +78 -0
- skylos/gatekeeper.py +147 -0
- skylos/linter.py +18 -0
- skylos/rules/base.py +20 -0
- skylos/rules/danger/calls.py +119 -0
- skylos/rules/danger/danger.py +157 -0
- skylos/rules/danger/danger_cmd/cmd_flow.py +75 -0
- skylos/rules/danger/danger_fs/__init__.py +0 -0
- skylos/rules/danger/danger_fs/path_flow.py +79 -0
- skylos/rules/danger/danger_net/__init__.py +0 -0
- skylos/rules/danger/danger_net/ssrf_flow.py +80 -0
- skylos/rules/danger/danger_sql/__init__.py +0 -0
- skylos/rules/danger/danger_sql/sql_flow.py +245 -0
- skylos/rules/danger/danger_sql/sql_raw_flow.py +96 -0
- skylos/rules/danger/danger_web/__init__.py +0 -0
- skylos/rules/danger/danger_web/xss_flow.py +170 -0
- skylos/rules/danger/taint.py +110 -0
- skylos/rules/quality/__init__.py +0 -0
- skylos/rules/quality/complexity.py +95 -0
- skylos/rules/quality/logic.py +96 -0
- skylos/rules/quality/nesting.py +101 -0
- skylos/rules/quality/structure.py +99 -0
- skylos/rules/secrets.py +325 -0
- skylos/server.py +554 -0
- skylos/visitor.py +502 -90
- skylos/visitors/__init__.py +0 -0
- skylos/visitors/framework_aware.py +437 -0
- skylos/visitors/test_aware.py +74 -0
- skylos-2.5.2.dist-info/METADATA +21 -0
- skylos-2.5.2.dist-info/RECORD +42 -0
- {skylos-1.0.10.dist-info → skylos-2.5.2.dist-info}/WHEEL +1 -1
- {skylos-1.0.10.dist-info → skylos-2.5.2.dist-info}/top_level.txt +0 -1
- skylos-1.0.10.dist-info/METADATA +0 -8
- skylos-1.0.10.dist-info/RECORD +0 -21
- test/compare_tools.py +0 -604
- test/diagnostics.py +0 -364
- test/sample_repo/app.py +0 -13
- test/sample_repo/sample_repo/commands.py +0 -81
- test/sample_repo/sample_repo/models.py +0 -122
- test/sample_repo/sample_repo/routes.py +0 -89
- test/sample_repo/sample_repo/utils.py +0 -36
- test/test_skylos.py +0 -456
- test/test_visitor.py +0 -220
- {test → skylos/rules}/__init__.py +0 -0
- {test/sample_repo → skylos/rules/danger}/__init__.py +0 -0
- {test/sample_repo/sample_repo → skylos/rules/danger/danger_cmd}/__init__.py +0 -0
- {skylos-1.0.10.dist-info → skylos-2.5.2.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from skylos.rules.base import SkylosRule
|
|
4
|
+
|
|
5
|
+
RULE_ID = "SKY-Q301"
|
|
6
|
+
|
|
7
|
+
_COMPLEX_NODES = (
|
|
8
|
+
ast.If,
|
|
9
|
+
ast.For,
|
|
10
|
+
ast.While,
|
|
11
|
+
ast.Try,
|
|
12
|
+
ast.With,
|
|
13
|
+
ast.ExceptHandler,
|
|
14
|
+
ast.BoolOp,
|
|
15
|
+
ast.IfExp,
|
|
16
|
+
ast.comprehension,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _func_complexity(node):
|
|
21
|
+
c = 1
|
|
22
|
+
for child in ast.walk(node):
|
|
23
|
+
if isinstance(child, _COMPLEX_NODES):
|
|
24
|
+
if isinstance(child, ast.BoolOp):
|
|
25
|
+
c += len(child.values) - 1
|
|
26
|
+
else:
|
|
27
|
+
c += 1
|
|
28
|
+
return c
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _func_length(node):
|
|
32
|
+
start = getattr(node, "lineno", None)
|
|
33
|
+
end = getattr(node, "end_lineno", None)
|
|
34
|
+
|
|
35
|
+
if start is None:
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
if end is None:
|
|
39
|
+
end = start
|
|
40
|
+
for child in ast.walk(node):
|
|
41
|
+
ln = getattr(child, "lineno", None)
|
|
42
|
+
if ln is not None and ln > end:
|
|
43
|
+
end = ln
|
|
44
|
+
|
|
45
|
+
return max(end - start + 1, 0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ComplexityRule(SkylosRule):
|
|
49
|
+
rule_id = "SKY-Q301"
|
|
50
|
+
name = "Cyclomatic Complexity"
|
|
51
|
+
|
|
52
|
+
def __init__(self, threshold=10):
|
|
53
|
+
self.threshold = threshold
|
|
54
|
+
|
|
55
|
+
def visit_node(self, node, context):
|
|
56
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
complexity = _func_complexity(node)
|
|
60
|
+
|
|
61
|
+
if complexity < self.threshold:
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
if complexity < 15:
|
|
65
|
+
severity = "WARN"
|
|
66
|
+
elif complexity < 25:
|
|
67
|
+
severity = "HIGH"
|
|
68
|
+
else:
|
|
69
|
+
severity = "CRITICAL"
|
|
70
|
+
|
|
71
|
+
length = _func_length(node)
|
|
72
|
+
mod = context.get("mod", "")
|
|
73
|
+
|
|
74
|
+
if mod:
|
|
75
|
+
func_name = f"{mod}.{node.name}"
|
|
76
|
+
else:
|
|
77
|
+
func_name = node.name
|
|
78
|
+
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
"rule_id": self.rule_id,
|
|
82
|
+
"kind": "complexity",
|
|
83
|
+
"severity": severity,
|
|
84
|
+
"type": "function",
|
|
85
|
+
"name": func_name,
|
|
86
|
+
"simple_name": node.name,
|
|
87
|
+
"value": complexity,
|
|
88
|
+
"threshold": self.threshold,
|
|
89
|
+
"length": length,
|
|
90
|
+
"message": f"Function is complex (McCabe={complexity}). Consider splitting loops/branches.",
|
|
91
|
+
"file": context.get("filename"),
|
|
92
|
+
"basename": Path(context.get("filename", "")).name,
|
|
93
|
+
"line": node.lineno,
|
|
94
|
+
}
|
|
95
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from skylos.rules.base import SkylosRule
|
|
4
|
+
|
|
5
|
+
class MutableDefaultRule(SkylosRule):
|
|
6
|
+
rule_id = "SKY-L001"
|
|
7
|
+
name = "Mutable Default Argument"
|
|
8
|
+
|
|
9
|
+
def visit_node(self, node, context):
|
|
10
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
11
|
+
return None
|
|
12
|
+
findings = []
|
|
13
|
+
|
|
14
|
+
kw_defaults_filtered = []
|
|
15
|
+
for d in node.args.kw_defaults:
|
|
16
|
+
if d:
|
|
17
|
+
kw_defaults_filtered.append(d)
|
|
18
|
+
|
|
19
|
+
for default in node.args.defaults + kw_defaults_filtered:
|
|
20
|
+
if isinstance(default, (ast.List, ast.Dict, ast.Set)):
|
|
21
|
+
findings.append({
|
|
22
|
+
"rule_id": self.rule_id,
|
|
23
|
+
"kind": "logic",
|
|
24
|
+
"severity": "HIGH",
|
|
25
|
+
"type": "function",
|
|
26
|
+
"name": node.name,
|
|
27
|
+
"simple_name": node.name,
|
|
28
|
+
"value": "mutable",
|
|
29
|
+
"threshold": 0,
|
|
30
|
+
"message": "Mutable default argument detected (List/Dict/Set). This causes state leaks.",
|
|
31
|
+
"file": context.get("filename"),
|
|
32
|
+
"basename": Path(context.get("filename", "")).name,
|
|
33
|
+
"line": default.lineno,
|
|
34
|
+
"col": default.col_offset
|
|
35
|
+
})
|
|
36
|
+
if findings:
|
|
37
|
+
return findings
|
|
38
|
+
else:
|
|
39
|
+
return None
|
|
40
|
+
|
|
41
|
+
class BareExceptRule(SkylosRule):
|
|
42
|
+
rule_id = "SKY-L002"
|
|
43
|
+
name = "Bare Except Block"
|
|
44
|
+
|
|
45
|
+
def visit_node(self, node, context):
|
|
46
|
+
if isinstance(node, ast.ExceptHandler) and node.type is None:
|
|
47
|
+
return [{
|
|
48
|
+
"rule_id": self.rule_id,
|
|
49
|
+
"kind": "logic",
|
|
50
|
+
"severity": "MEDIUM",
|
|
51
|
+
"type": "block",
|
|
52
|
+
"name": "except",
|
|
53
|
+
"simple_name": "except",
|
|
54
|
+
"value": "bare",
|
|
55
|
+
"threshold": 0,
|
|
56
|
+
"message": "Bare 'except:' block swallows SystemExit and other critical errors.",
|
|
57
|
+
"file": context.get("filename"),
|
|
58
|
+
"basename": Path(context.get("filename", "")).name,
|
|
59
|
+
"line": node.lineno,
|
|
60
|
+
"col": node.col_offset
|
|
61
|
+
}]
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
class DangerousComparisonRule(SkylosRule):
|
|
65
|
+
rule_id = "SKY-L003"
|
|
66
|
+
name = "Dangerous Comparison"
|
|
67
|
+
|
|
68
|
+
def visit_node(self, node, context):
|
|
69
|
+
if not isinstance(node, ast.Compare):
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
findings = []
|
|
73
|
+
for op, comparator in zip(node.ops, node.comparators):
|
|
74
|
+
if isinstance(op, (ast.Eq, ast.NotEq)):
|
|
75
|
+
if isinstance(comparator, ast.Constant):
|
|
76
|
+
val = comparator.value
|
|
77
|
+
if val is True or val is False or val is None:
|
|
78
|
+
findings.append({
|
|
79
|
+
"rule_id": self.rule_id,
|
|
80
|
+
"kind": "logic",
|
|
81
|
+
"severity": "LOW",
|
|
82
|
+
"type": "comparison",
|
|
83
|
+
"name": "==",
|
|
84
|
+
"simple_name": "==",
|
|
85
|
+
"value": str(comparator.value),
|
|
86
|
+
"threshold": 0,
|
|
87
|
+
"message": f"Comparison to {comparator.value} should use 'is' or 'is not'.",
|
|
88
|
+
"file": context.get("filename"),
|
|
89
|
+
"basename": Path(context.get("filename", "")).name,
|
|
90
|
+
"line": node.lineno,
|
|
91
|
+
"col": node.col_offset
|
|
92
|
+
})
|
|
93
|
+
if findings:
|
|
94
|
+
return findings
|
|
95
|
+
else:
|
|
96
|
+
return None
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import ast
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from skylos.rules.base import SkylosRule
|
|
5
|
+
|
|
6
|
+
RULE_ID = "SKY-Q302"
|
|
7
|
+
|
|
8
|
+
NEST_NODES = (ast.If, ast.For, ast.While, ast.Try, ast.With)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _max_depth(nodes, depth=0):
|
|
12
|
+
max_depth = depth
|
|
13
|
+
for node in nodes:
|
|
14
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
15
|
+
continue
|
|
16
|
+
|
|
17
|
+
if isinstance(node, NEST_NODES):
|
|
18
|
+
branches = []
|
|
19
|
+
if isinstance(node, ast.If):
|
|
20
|
+
branches.append(node.body)
|
|
21
|
+
if node.orelse:
|
|
22
|
+
branches.append(node.orelse)
|
|
23
|
+
|
|
24
|
+
elif isinstance(node, (ast.For, ast.While)):
|
|
25
|
+
branches.append(node.body)
|
|
26
|
+
if node.orelse:
|
|
27
|
+
branches.append(node.orelse)
|
|
28
|
+
|
|
29
|
+
elif isinstance(node, ast.With):
|
|
30
|
+
branches.append(node.body)
|
|
31
|
+
|
|
32
|
+
elif isinstance(node, ast.Try):
|
|
33
|
+
branches.append(node.body)
|
|
34
|
+
for handler in node.handlers:
|
|
35
|
+
branches.append(handler.body)
|
|
36
|
+
if node.orelse:
|
|
37
|
+
branches.append(node.orelse)
|
|
38
|
+
if node.finalbody:
|
|
39
|
+
branches.append(node.finalbody)
|
|
40
|
+
|
|
41
|
+
for branch in branches:
|
|
42
|
+
max_depth = max(max_depth, _max_depth(branch, depth + 1))
|
|
43
|
+
|
|
44
|
+
return max_depth
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _physical_length(node):
|
|
48
|
+
start = getattr(node, "lineno", 0)
|
|
49
|
+
end = getattr(node, "end_lineno", start)
|
|
50
|
+
return max(end - start + 1, 0)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class NestingRule(SkylosRule):
|
|
54
|
+
rule_id = "SKY-Q302"
|
|
55
|
+
name = "Deep Nesting"
|
|
56
|
+
|
|
57
|
+
def __init__(self, threshold=3):
|
|
58
|
+
self.threshold = threshold
|
|
59
|
+
|
|
60
|
+
def visit_node(self, node, context):
|
|
61
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
depth = _max_depth(node.body, 0)
|
|
65
|
+
|
|
66
|
+
if depth <= self.threshold:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
if depth <= self.threshold + 2:
|
|
70
|
+
severity = "MEDIUM"
|
|
71
|
+
elif depth <= self.threshold + 5:
|
|
72
|
+
severity = "HIGH"
|
|
73
|
+
else:
|
|
74
|
+
severity = "CRITICAL"
|
|
75
|
+
|
|
76
|
+
physical_len = _physical_length(node)
|
|
77
|
+
mod = context.get("mod", "")
|
|
78
|
+
|
|
79
|
+
if mod:
|
|
80
|
+
func_name = f"{mod}.{node.name}"
|
|
81
|
+
else:
|
|
82
|
+
func_name = node.name
|
|
83
|
+
|
|
84
|
+
return [
|
|
85
|
+
{
|
|
86
|
+
"rule_id": self.rule_id,
|
|
87
|
+
"kind": "nesting",
|
|
88
|
+
"severity": severity,
|
|
89
|
+
"type": "function",
|
|
90
|
+
"name": func_name,
|
|
91
|
+
"simple_name": node.name,
|
|
92
|
+
"file": context.get("filename"),
|
|
93
|
+
"basename": Path(context.get("filename", "")).name,
|
|
94
|
+
"line": node.lineno,
|
|
95
|
+
"metric": "max_nesting",
|
|
96
|
+
"value": depth,
|
|
97
|
+
"threshold": self.threshold,
|
|
98
|
+
"length": physical_len,
|
|
99
|
+
"message": f"Nesting depth of {depth} exceeds threshold of {self.threshold}. Consider using early returns.",
|
|
100
|
+
}
|
|
101
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from skylos.rules.base import SkylosRule
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ArgCountRule(SkylosRule):
|
|
7
|
+
rule_id = "SKY-C303"
|
|
8
|
+
name = "Too Many Arguments"
|
|
9
|
+
|
|
10
|
+
def __init__(self, max_args=5):
|
|
11
|
+
self.max_args = max_args
|
|
12
|
+
|
|
13
|
+
def visit_node(self, node, context):
|
|
14
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
args = node.args.args
|
|
18
|
+
clean_args = []
|
|
19
|
+
for a in args:
|
|
20
|
+
if a.arg not in ("self", "cls"):
|
|
21
|
+
clean_args.append(a)
|
|
22
|
+
|
|
23
|
+
total_count = len(clean_args) + len(node.args.kwonlyargs)
|
|
24
|
+
|
|
25
|
+
if total_count <= self.max_args:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
mod = context.get("mod", "")
|
|
29
|
+
if mod:
|
|
30
|
+
func_name = f"{mod}.{node.name}"
|
|
31
|
+
else:
|
|
32
|
+
func_name = node.name
|
|
33
|
+
|
|
34
|
+
return [
|
|
35
|
+
{
|
|
36
|
+
"rule_id": self.rule_id,
|
|
37
|
+
"kind": "structure",
|
|
38
|
+
"type": "function",
|
|
39
|
+
"name": func_name,
|
|
40
|
+
"simple_name": node.name,
|
|
41
|
+
"value": total_count,
|
|
42
|
+
"threshold": self.max_args,
|
|
43
|
+
"severity": "MEDIUM",
|
|
44
|
+
"message": f"Function has {total_count} arguments (limit: {self.max_args}). Refactor.",
|
|
45
|
+
"file": context.get("filename"),
|
|
46
|
+
"basename": Path(context.get("filename", "")).name,
|
|
47
|
+
"line": node.lineno,
|
|
48
|
+
"col": node.col_offset,
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class FunctionLengthRule(SkylosRule):
|
|
54
|
+
rule_id = "SKY-C304"
|
|
55
|
+
name = "Function Too Long"
|
|
56
|
+
|
|
57
|
+
def __init__(self, max_lines=50):
|
|
58
|
+
self.max_lines = max_lines
|
|
59
|
+
|
|
60
|
+
def visit_node(self, node, context):
|
|
61
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
start = getattr(node, "lineno", 0)
|
|
65
|
+
end = getattr(node, "end_lineno", start)
|
|
66
|
+
physical_length = max(end - start + 1, 0)
|
|
67
|
+
|
|
68
|
+
if physical_length <= self.max_lines:
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
if physical_length < 100:
|
|
72
|
+
severity = "MEDIUM"
|
|
73
|
+
else:
|
|
74
|
+
severity = "HIGH"
|
|
75
|
+
|
|
76
|
+
mod = context.get("mod", "")
|
|
77
|
+
|
|
78
|
+
if mod:
|
|
79
|
+
func_name = f"{mod}.{node.name}"
|
|
80
|
+
else:
|
|
81
|
+
func_name = node.name
|
|
82
|
+
|
|
83
|
+
return [
|
|
84
|
+
{
|
|
85
|
+
"rule_id": self.rule_id,
|
|
86
|
+
"kind": "structure",
|
|
87
|
+
"type": "function",
|
|
88
|
+
"name": func_name,
|
|
89
|
+
"simple_name": node.name,
|
|
90
|
+
"value": physical_length,
|
|
91
|
+
"threshold": self.max_lines,
|
|
92
|
+
"severity": severity,
|
|
93
|
+
"message": f"Function is {physical_length} lines long (limit: {self.max_lines}). It is too big.",
|
|
94
|
+
"file": context.get("filename"),
|
|
95
|
+
"basename": Path(context.get("filename", "")).name,
|
|
96
|
+
"line": node.lineno,
|
|
97
|
+
"col": node.col_offset,
|
|
98
|
+
}
|
|
99
|
+
]
|