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.

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, mock_class, mock_init, mock_enter = skylos_with_class_methods
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, _ = proc_file(f.name, "test_module")
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, _ = proc_file(f.name, "test_module")
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, _ = proc_file((f.name, "test_module"))
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