skylos 2.1.1__py3-none-any.whl → 2.2.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.
Potentially problematic release.
This version of skylos might be problematic. Click here for more details.
- skylos/__init__.py +5 -3
- skylos/analyzer.py +54 -9
- skylos/cli.py +107 -22
- skylos/codemods.py +154 -5
- skylos/constants.py +2 -3
- skylos/rules/__init__.py +0 -0
- skylos/rules/secrets.py +268 -0
- skylos/server.py +1 -12
- skylos/visitor.py +97 -24
- skylos/visitors/__init__.py +0 -0
- {skylos-2.1.1.dist-info → skylos-2.2.2.dist-info}/METADATA +1 -1
- {skylos-2.1.1.dist-info → skylos-2.2.2.dist-info}/RECORD +20 -15
- test/test_analyzer.py +6 -43
- test/test_new_behaviours.py +52 -0
- test/test_secrets.py +179 -0
- /skylos/{framework_aware.py → visitors/framework_aware.py} +0 -0
- /skylos/{test_aware.py → visitors/test_aware.py} +0 -0
- {skylos-2.1.1.dist-info → skylos-2.2.2.dist-info}/WHEEL +0 -0
- {skylos-2.1.1.dist-info → skylos-2.2.2.dist-info}/entry_points.txt +0 -0
- {skylos-2.1.1.dist-info → skylos-2.2.2.dist-info}/top_level.txt +0 -0
test/test_analyzer.py
CHANGED
|
@@ -8,11 +8,9 @@ from skylos.test_aware import TestAwareVisitor
|
|
|
8
8
|
from skylos.framework_aware import FrameworkAwareVisitor
|
|
9
9
|
|
|
10
10
|
from skylos.analyzer import (
|
|
11
|
-
Skylos,
|
|
12
|
-
parse_exclude_folders,
|
|
11
|
+
Skylos,
|
|
13
12
|
proc_file,
|
|
14
|
-
analyze
|
|
15
|
-
DEFAULT_EXCLUDE_FOLDERS,
|
|
13
|
+
analyze
|
|
16
14
|
)
|
|
17
15
|
|
|
18
16
|
@pytest.fixture
|
|
@@ -97,40 +95,6 @@ def internal_function():
|
|
|
97
95
|
|
|
98
96
|
yield temp_path
|
|
99
97
|
|
|
100
|
-
class TestParseExcludeFolders:
|
|
101
|
-
|
|
102
|
-
def test_default_exclude_folders_included(self):
|
|
103
|
-
"""default folders are included by default."""
|
|
104
|
-
result = parse_exclude_folders(None, use_defaults=True)
|
|
105
|
-
assert DEFAULT_EXCLUDE_FOLDERS.issubset(result)
|
|
106
|
-
|
|
107
|
-
def test_default_exclude_folders_disabled(self):
|
|
108
|
-
"""default folders can be disabled."""
|
|
109
|
-
result = parse_exclude_folders(None, use_defaults=False)
|
|
110
|
-
assert not DEFAULT_EXCLUDE_FOLDERS.intersection(result)
|
|
111
|
-
|
|
112
|
-
def test_user_exclude_folders_added(self):
|
|
113
|
-
"""user-specified folders are added."""
|
|
114
|
-
user_folders = {"custom_folder", "another_folder"}
|
|
115
|
-
result = parse_exclude_folders(user_folders, use_defaults=True)
|
|
116
|
-
assert user_folders.issubset(result)
|
|
117
|
-
assert DEFAULT_EXCLUDE_FOLDERS.issubset(result)
|
|
118
|
-
|
|
119
|
-
def test_include_folders_override_defaults(self):
|
|
120
|
-
"""include_folders can override defaults."""
|
|
121
|
-
include_folders = {"__pycache__", ".git"}
|
|
122
|
-
result = parse_exclude_folders(None, use_defaults=True, include_folders=include_folders)
|
|
123
|
-
for folder in include_folders:
|
|
124
|
-
assert folder not in result
|
|
125
|
-
|
|
126
|
-
def test_include_folders_override_user_excludes(self):
|
|
127
|
-
"""include_folders can override user excludes."""
|
|
128
|
-
user_excludes = {"custom_folder", "another_folder"}
|
|
129
|
-
include_folders = {"custom_folder"}
|
|
130
|
-
result = parse_exclude_folders(user_excludes, use_defaults=False, include_folders=include_folders)
|
|
131
|
-
assert "custom_folder" not in result
|
|
132
|
-
assert "another_folder" in result
|
|
133
|
-
|
|
134
98
|
class TestSkylos:
|
|
135
99
|
|
|
136
100
|
@pytest.fixture
|
|
@@ -144,7 +108,6 @@ class TestSkylos:
|
|
|
144
108
|
assert isinstance(skylos.exports, defaultdict)
|
|
145
109
|
|
|
146
110
|
def test_module_name_generation(self, skylos):
|
|
147
|
-
"""Test module name generation from file paths."""
|
|
148
111
|
root = Path("/project")
|
|
149
112
|
|
|
150
113
|
# test a regular Python file
|
|
@@ -315,7 +278,7 @@ class TestHeuristics:
|
|
|
315
278
|
|
|
316
279
|
def test_auto_called_methods_get_references(self, skylos_with_class_methods):
|
|
317
280
|
"""auto-called methods get reference counts when class is used."""
|
|
318
|
-
skylos,
|
|
281
|
+
skylos, _, mock_init, mock_enter = skylos_with_class_methods
|
|
319
282
|
|
|
320
283
|
skylos._apply_heuristics()
|
|
321
284
|
|
|
@@ -451,7 +414,7 @@ class TestClass:
|
|
|
451
414
|
mock_framework_visitor = Mock(spec=FrameworkAwareVisitor)
|
|
452
415
|
mock_framework_visitor_class.return_value = mock_framework_visitor
|
|
453
416
|
|
|
454
|
-
defs, refs, dyn, exports, test_flags, framework_flags
|
|
417
|
+
defs, refs, dyn, exports, test_flags, framework_flags = proc_file(f.name, "test_module")
|
|
455
418
|
|
|
456
419
|
mock_visitor_class.assert_called_once_with("test_module", f.name)
|
|
457
420
|
mock_visitor.visit.assert_called_once()
|
|
@@ -472,7 +435,7 @@ class TestClass:
|
|
|
472
435
|
f.flush()
|
|
473
436
|
|
|
474
437
|
try:
|
|
475
|
-
defs, refs, dyn, exports, test_flags, framework_flags
|
|
438
|
+
defs, refs, dyn, exports, test_flags, framework_flags = proc_file(f.name, "test_module")
|
|
476
439
|
|
|
477
440
|
assert defs == []
|
|
478
441
|
assert refs == []
|
|
@@ -506,7 +469,7 @@ class TestClass:
|
|
|
506
469
|
mock_framework_visitor = Mock(spec=FrameworkAwareVisitor)
|
|
507
470
|
mock_framework_visitor_class.return_value = mock_framework_visitor
|
|
508
471
|
|
|
509
|
-
defs, refs, dyn, exports, test_flags, framework_flags
|
|
472
|
+
defs, refs, dyn, exports, test_flags, framework_flags = proc_file((f.name, "test_module"))
|
|
510
473
|
|
|
511
474
|
mock_visitor_class.assert_called_once_with("test_module", f.name)
|
|
512
475
|
finally:
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from skylos.analyzer import analyze
|
|
3
|
+
|
|
4
|
+
def test_dataclass_fields(tmp_path):
|
|
5
|
+
p = tmp_path / "dc.py"
|
|
6
|
+
p.write_text(
|
|
7
|
+
"from dataclasses import dataclass\n"
|
|
8
|
+
"@dataclass\n"
|
|
9
|
+
"class C:\n"
|
|
10
|
+
" x: int = 1\n"
|
|
11
|
+
" y: int = 2\n"
|
|
12
|
+
"def f(c: C):\n"
|
|
13
|
+
" return c.x + c.y\n"
|
|
14
|
+
"f(C())\n"
|
|
15
|
+
)
|
|
16
|
+
result = json.loads(analyze(str(tmp_path), conf=0))
|
|
17
|
+
assert result['unused_variables'] == []
|
|
18
|
+
|
|
19
|
+
def test_dead_store_guard(tmp_path):
|
|
20
|
+
p = tmp_path / "dstore.py"
|
|
21
|
+
p.write_text(
|
|
22
|
+
"lst=[1,2,3]\n"
|
|
23
|
+
"dir=0\n"
|
|
24
|
+
"if 'a' in lst:\n"
|
|
25
|
+
" pass\n"
|
|
26
|
+
"else:\n"
|
|
27
|
+
" dir=1\n"
|
|
28
|
+
"print(dir)\n"
|
|
29
|
+
)
|
|
30
|
+
result = json.loads(analyze(str(tmp_path), conf=0))
|
|
31
|
+
assert result['unused_variables'] == []
|
|
32
|
+
|
|
33
|
+
def test_unused_constant_reported(tmp_path):
|
|
34
|
+
p = tmp_path / "pool.py"
|
|
35
|
+
p.write_text(
|
|
36
|
+
"from concurrent.futures import ProcessPoolExecutor\n"
|
|
37
|
+
"PROCESS_POOL=None\n"
|
|
38
|
+
"NEVER_USED=123\n"
|
|
39
|
+
"def get_pool():\n"
|
|
40
|
+
" global PROCESS_POOL\n"
|
|
41
|
+
" if PROCESS_POOL is None:\n"
|
|
42
|
+
" PROCESS_POOL=ProcessPoolExecutor(max_workers=2)\n"
|
|
43
|
+
" return PROCESS_POOL\n"
|
|
44
|
+
"get_pool()\n"
|
|
45
|
+
)
|
|
46
|
+
result = json.loads(analyze(str(tmp_path), conf=0))
|
|
47
|
+
names = set()
|
|
48
|
+
for v in result['unused_variables']:
|
|
49
|
+
names.add(v['name'])
|
|
50
|
+
|
|
51
|
+
assert 'NEVER_USED' in names
|
|
52
|
+
assert 'PROCESS_POOL' not in names
|
test/test_secrets.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import pytest
|
|
3
|
+
from skylos.rules.secrets import scan_ctx
|
|
4
|
+
|
|
5
|
+
ELLIPSIS = "…"
|
|
6
|
+
|
|
7
|
+
def _ctx_from_source(src, rel="app.py", with_ast=False):
|
|
8
|
+
if with_ast:
|
|
9
|
+
tree = ast.parse(src)
|
|
10
|
+
else:
|
|
11
|
+
tree = None
|
|
12
|
+
|
|
13
|
+
lines = src.splitlines(True)
|
|
14
|
+
|
|
15
|
+
context = {
|
|
16
|
+
"relpath": rel,
|
|
17
|
+
"lines": lines,
|
|
18
|
+
"tree": tree
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return context
|
|
22
|
+
|
|
23
|
+
def test_github_and_generic_both_fire_on_token_assignment():
|
|
24
|
+
src = 'GITHUB_TOKEN = "ghp_1234567890abcdef1234567890abcdef1234"\n'
|
|
25
|
+
|
|
26
|
+
findings = list(scan_ctx(_ctx_from_source(src)))
|
|
27
|
+
|
|
28
|
+
providers = set()
|
|
29
|
+
for finding in findings:
|
|
30
|
+
provider_name = finding["provider"]
|
|
31
|
+
providers.add(provider_name)
|
|
32
|
+
|
|
33
|
+
assert "github" in providers
|
|
34
|
+
assert "generic" in providers
|
|
35
|
+
|
|
36
|
+
github_previews = []
|
|
37
|
+
for finding in findings:
|
|
38
|
+
if finding["provider"] == "github":
|
|
39
|
+
preview = finding["preview"]
|
|
40
|
+
github_previews.append(preview)
|
|
41
|
+
|
|
42
|
+
assert len(github_previews) > 0
|
|
43
|
+
first_preview = github_previews[0]
|
|
44
|
+
assert first_preview.startswith("ghp_")
|
|
45
|
+
assert ELLIPSIS in first_preview
|
|
46
|
+
|
|
47
|
+
@pytest.mark.parametrize(
|
|
48
|
+
"line,provider",
|
|
49
|
+
[
|
|
50
|
+
('GITLAB_PAT = "glpat-A1b2C3d4E5f6G7h8I9j0"\n', "gitlab"),
|
|
51
|
+
('SLACK_BOT = "xoxb-1234567890ABCDEF12"\n', "slack"),
|
|
52
|
+
('STRIPE = "sk_live_a1B2c3D4e5F6g7H8"\n', "stripe"),
|
|
53
|
+
('GOOGLE = "AIzaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"\n', "google_api_key"),
|
|
54
|
+
('SENDGRID = "SG.AAAAABBBBBCCCCCC.DDDDDEEEEEFFFFFFF"\n', "sendgrid"),
|
|
55
|
+
('TWILIO = "SK0123456789abcdef0123456789abcdef"\n', "twilio"),
|
|
56
|
+
('PK = "-----BEGIN RSA PRIVATE KEY-----"\n', "private_key_block"),
|
|
57
|
+
('AWS_ACCESS_KEY_ID = "AKIAABCDEFGHIJKLMNOP"\n', "aws_access_key_id"),
|
|
58
|
+
],
|
|
59
|
+
)
|
|
60
|
+
def test_provider_patterns(line, provider):
|
|
61
|
+
findings = list(scan_ctx(_ctx_from_source(line)))
|
|
62
|
+
assert any(f["provider"] == provider for f in findings)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_aws_secret_access_key_special_case():
|
|
66
|
+
src = (
|
|
67
|
+
'AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"\n'
|
|
68
|
+
)
|
|
69
|
+
findings = list(scan_ctx(_ctx_from_source(src)))
|
|
70
|
+
hit = None
|
|
71
|
+
for finding in findings:
|
|
72
|
+
if finding["provider"] == "aws_secret_access_key":
|
|
73
|
+
hit = finding
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
assert hit is not None
|
|
77
|
+
assert "entropy" in hit and isinstance(hit["entropy"], float)
|
|
78
|
+
assert ELLIPSIS in hit["preview"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_generic_entropy_detection_and_threshold():
|
|
82
|
+
src = 'X = "o2uV7Ew1kZ9Q3nR8sT5yU6pX4cJ2mL7a"\n'
|
|
83
|
+
findings = list(scan_ctx(_ctx_from_source(src)))
|
|
84
|
+
assert any(f["provider"] == "generic" for f in findings)
|
|
85
|
+
|
|
86
|
+
findings_high_thr = list(scan_ctx(_ctx_from_source(src), min_entropy=8.0))
|
|
87
|
+
assert not any(f["provider"] == "generic" for f in findings_high_thr)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_ignore_directive_suppresses_matches():
|
|
91
|
+
src = 'GITHUB_TOKEN = "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # skylos: ignore[SKY-S101]\n'
|
|
92
|
+
findings = list(scan_ctx(_ctx_from_source(src)))
|
|
93
|
+
assert findings == []
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_allowlist_patterns_suppresses_line():
|
|
97
|
+
src = 'TWILIO = "SKabcdefabcdefabcdefabcdefabcdefabcd"\n'
|
|
98
|
+
allow = [r"TWILIO\s*="]
|
|
99
|
+
findings = list(scan_ctx(_ctx_from_source(src), allowlist_patterns=allow))
|
|
100
|
+
assert findings == []
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_scan_comments_toggle():
|
|
104
|
+
line = '# cred: xoxb-1234567890ABCDEF12 appears only in comment\n'
|
|
105
|
+
findings_default = list(scan_ctx(_ctx_from_source(line)))
|
|
106
|
+
|
|
107
|
+
found_slack = False
|
|
108
|
+
for finding in findings_default:
|
|
109
|
+
if finding["provider"] == "slack":
|
|
110
|
+
found_slack = True
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
assert found_slack
|
|
114
|
+
|
|
115
|
+
findings_off = list(scan_ctx(_ctx_from_source(line), scan_comments=False))
|
|
116
|
+
assert findings_off == []
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_scan_docstrings_toggle_with_ast():
|
|
120
|
+
src = '''"""
|
|
121
|
+
module docstring with a GITHUB token: ghp_1234567890abcdef1234567890abcdef1234
|
|
122
|
+
"""
|
|
123
|
+
def f():
|
|
124
|
+
"""Function docstring with AWS AKIAABCDEFGHIJKLMNOP key."""
|
|
125
|
+
return 1
|
|
126
|
+
'''
|
|
127
|
+
f1 = list(scan_ctx(_ctx_from_source(src, with_ast=True)))
|
|
128
|
+
providers_in_f1 = set()
|
|
129
|
+
for finding in f1:
|
|
130
|
+
provider_name = finding["provider"]
|
|
131
|
+
providers_in_f1.add(provider_name)
|
|
132
|
+
assert "github" in providers_in_f1 or "aws_access_key_id" in providers_in_f1
|
|
133
|
+
|
|
134
|
+
f2 = list(scan_ctx(_ctx_from_source(src, with_ast=True), scan_docstrings=False))
|
|
135
|
+
assert f2 == []
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_suffix_and_path_filters():
|
|
139
|
+
ctx_txt = _ctx_from_source('X="ghp_1234567890abcdef1234567890abcdef1234"\n', rel="notes.txt")
|
|
140
|
+
assert list(scan_ctx(ctx_txt)) == []
|
|
141
|
+
|
|
142
|
+
ctx_vendor = _ctx_from_source('X="AKIAABCDEFGHIJKLMNOP"\n', rel="vendor/app.py")
|
|
143
|
+
out = list(scan_ctx(ctx_vendor, ignore_path_substrings=["vendor"]))
|
|
144
|
+
assert out == []
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def test_masking_behavior_short_and_long():
|
|
148
|
+
short = 'X = "ABCDEFGH"\n'
|
|
149
|
+
long = 'token = "ABCDEFGHIJKLMNOPKLMN"\n'
|
|
150
|
+
|
|
151
|
+
short_findings = scan_ctx(_ctx_from_source(short))
|
|
152
|
+
|
|
153
|
+
f_short = None
|
|
154
|
+
for finding in short_findings:
|
|
155
|
+
if finding["provider"] == "generic":
|
|
156
|
+
f_short = finding
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
long_findings = scan_ctx(_ctx_from_source(long))
|
|
160
|
+
|
|
161
|
+
f_long = None
|
|
162
|
+
for finding in long_findings:
|
|
163
|
+
if finding["provider"] == "generic":
|
|
164
|
+
f_long = finding
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
assert f_short is None
|
|
168
|
+
|
|
169
|
+
long_preview = f_long["preview"]
|
|
170
|
+
starts_with_abcd = long_preview.startswith("ABCD")
|
|
171
|
+
ends_with_klmn = long_preview.endswith("KLMN")
|
|
172
|
+
contains_ellipsis = ELLIPSIS in long_preview
|
|
173
|
+
|
|
174
|
+
assert starts_with_abcd and ends_with_klmn and contains_ellipsis
|
|
175
|
+
|
|
176
|
+
def test_safe_hints_suppress_detection():
|
|
177
|
+
safe_line = 'EXAMPLE_TOKEN = "sk_test_this_is_example_value_not_real_123456"\n'
|
|
178
|
+
out = list(scan_ctx(_ctx_from_source(safe_line)))
|
|
179
|
+
assert out == []
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|