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.

skylos/rules/secrets.py CHANGED
@@ -22,7 +22,15 @@ GENERIC_VALUE = re.compile(r"""(?ix)
22
22
  (?:
23
23
  (token|api[_-]?key|secret|password|passwd|pwd|bearer|auth[_-]?token|access[_-]?token)
24
24
  \s*[:=]\s*(?P<q>['"])(?P<val>[^'"]{16,})(?P=q)
25
- )|(?P<bare>[A-Za-z0-9_\-]{24,})
25
+ )
26
+ |
27
+ (?P<bare>
28
+ (?=[A-Za-z0-9_-]{32,}\b)
29
+ (?=.*[A-Z])
30
+ (?=.*[a-z])
31
+ (?=.*\d)
32
+ [A-Za-z0-9_-]+
33
+ )
26
34
  """)
27
35
 
28
36
  SAFE_TEST_HINTS = {
@@ -30,8 +38,12 @@ SAFE_TEST_HINTS = {
30
38
  "changeme", "password", "secret", "not_a_real", "do_not_use",
31
39
  }
32
40
 
41
+ _IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
42
+
33
43
  IGNORE_DIRECTIVE = "skylos: ignore[SKY-S101]"
34
- DEFAULT_MIN_ENTROPY = 3.6
44
+ DEFAULT_MIN_ENTROPY = 3.9
45
+
46
+ IS_TEST_PATH = re.compile(r"(^|/)(tests?(/|$)|test_[^/]+\.py$)")
35
47
 
36
48
  def _entropy(s):
37
49
  if len(s) == 0:
@@ -64,6 +76,9 @@ def _mask(tok):
64
76
  last_part = tok[-4:]
65
77
  return first_part + "…" + last_part
66
78
 
79
+ def _looks_like_identifier(s):
80
+ return bool(_IDENTIFIER.fullmatch(s))
81
+
67
82
  def _docstring_lines(tree):
68
83
  if tree is None:
69
84
  return set()
@@ -107,12 +122,15 @@ def _docstring_lines(tree):
107
122
  return docstring_line_numbers
108
123
 
109
124
  def scan_ctx(ctx, *, min_entropy= DEFAULT_MIN_ENTROPY, scan_comments= True,
110
- scan_docstrings= True, allowlist_patterns= None, ignore_path_substrings= None):
125
+ scan_docstrings= True, allowlist_patterns= None, ignore_path_substrings= None, ignore_tests=True):
111
126
 
112
127
  rel_path = ctx.get("relpath", "")
113
128
  if not rel_path.endswith(ALLOWED_FILE_SUFFIXES):
114
129
  return []
115
130
 
131
+ if ignore_tests and IS_TEST_PATH.search(rel_path.replace("\\", "/")):
132
+ return []
133
+
116
134
  if ignore_path_substrings:
117
135
  for substring in ignore_path_substrings:
118
136
  if substring and substring in rel_path:
@@ -220,22 +238,33 @@ def scan_ctx(ctx, *, min_entropy= DEFAULT_MIN_ENTROPY, scan_comments= True,
220
238
  "entropy": round(tok_entropy, 2),
221
239
  }
222
240
  findings.append(aws_finding)
223
-
224
- generic_match = GENERIC_VALUE.search(line_content)
241
+
242
+ in_tests = bool(IS_TEST_PATH.search(rel_path.replace("\\", "/")))
243
+
244
+ if in_tests:
245
+ generic_match = None
246
+ else:
247
+ generic_match= GENERIC_VALUE.search(line_content)
248
+
225
249
  if generic_match:
226
250
  val_group = generic_match.group("val")
227
251
  bare_group = generic_match.group("bare")
228
252
 
253
+ is_bare = False
229
254
  if val_group:
230
255
  extracted_token = val_group
231
256
  elif bare_group:
232
257
  extracted_token = bare_group
258
+ is_bare = True
233
259
  else:
234
260
  extracted_token = ""
235
261
 
236
262
  clean_token = extracted_token.strip()
237
263
 
238
264
  if clean_token:
265
+ if is_bare and _looks_like_identifier(clean_token):
266
+ continue
267
+
239
268
  token_lowercase = clean_token.lower()
240
269
  has_safe_hint = False
241
270
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: skylos
3
- Version: 2.2.3
3
+ Version: 2.4.0
4
4
  Summary: A static analysis tool for Python codebases
5
5
  Author-email: oha <aaronoh2015@gmail.com>
6
6
  Requires-Python: >=3.9
@@ -1,12 +1,25 @@
1
- skylos/__init__.py,sha256=cFxP_uUY-acB9iRs7SE3ccyWiX6v8mI-mJo9bfhC2aU,229
2
- skylos/analyzer.py,sha256=G_8pw7GmChATc5h5XXij2pcHirhh_5G9Y8dlAC1dx38,16735
3
- skylos/cli.py,sha256=DOV6nwPbi5zh-OJ3wXjIaPxCfXRtiPMNqo9Zp2nvDBA,19475
1
+ skylos/__init__.py,sha256=lFWlWOwKfByIoAoQkdpcEMF5UisQQuVzdtSNPApOoXI,229
2
+ skylos/analyzer.py,sha256=nwf1pDsXaEhrLm6AkQgaJObVgp8YPP9UzU2NzZ2088o,18791
3
+ skylos/cli.py,sha256=372vGiWC4Z4ZDQbP-P4PYRaEokv8xS9-ko152ejfqcE,25970
4
4
  skylos/codemods.py,sha256=-1ehjFLc1MmtK3inoSZF_RyBNWF7XZSOq2KH5_MDzuA,9519
5
5
  skylos/constants.py,sha256=kU-2FKQAV-ju4rYw62Tw25uRvqauzjZFUqtvGWaI6Es,1571
6
6
  skylos/server.py,sha256=oHuevjdDFvJVbvpTtCDjSLOJ6Zy1jL4BYLYV4VFNMXs,15903
7
7
  skylos/visitor.py,sha256=o_2JxjXKXAcWLQr8nmoatGAz2EhBk225qE_piHf3hDg,20458
8
8
  skylos/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- skylos/rules/secrets.py,sha256=_cvi-NETScQWMPexb8R2_QtgTRMQGbb09BT1Os246DA,9370
9
+ skylos/rules/secrets.py,sha256=xZOKJwfeyx2el-HlGjTflc50XtjUL7R-8mLOJUkwf30,10092
10
+ skylos/rules/danger/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ skylos/rules/danger/danger.py,sha256=oTG_dK-CAqV4X4DP_AOzKPsOzmYbtgq0pfq0aILmxik,4398
12
+ skylos/rules/danger/danger_cmd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ skylos/rules/danger/danger_cmd/cmd_flow.py,sha256=b2VOkZ1Lt5ht5AreQrzqcu19BrR8N8xWjYeMqwtRJr8,6675
14
+ skylos/rules/danger/danger_fs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
+ skylos/rules/danger/danger_fs/path_flow.py,sha256=bhs1HHrxsHVilvQG6-PsZIJ7GpF20Yj7pcavPXFKq2g,5627
16
+ skylos/rules/danger/danger_net/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ skylos/rules/danger/danger_net/ssrf_flow.py,sha256=jyLmMDq7WlCqB2t3O1bKyrOmrr9yAqhEUjytek1Ymbg,5937
18
+ skylos/rules/danger/danger_sql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ skylos/rules/danger/danger_sql/sql_flow.py,sha256=00QpJTgdwE6lGuiv416o3MW54MkioiMzSlI5jNsYCK4,5212
20
+ skylos/rules/danger/danger_sql/sql_raw_flow.py,sha256=7pYidj8_KCu9UkpHUyFoiDch3Hjc0CHEhTBM-bRXlew,6504
21
+ skylos/rules/danger/danger_web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ skylos/rules/danger/danger_web/xss_flow.py,sha256=9cK65470f8ulK0aNa8pRFsH3p-7gpenMJvExbJqmjG8,8991
10
23
  skylos/visitors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
24
  skylos/visitors/framework_aware.py,sha256=eX4oU0jQwDWejkkW4kjRNctre27sVLHK1CTDDiqPqRw,13054
12
25
  skylos/visitors/test_aware.py,sha256=kxYoMG2m02kbMlxtFOM-MWJO8qqxHviP9HgAMbKRfvU,2304
@@ -17,13 +30,18 @@ test/diagnostics.py,sha256=ExuFOCVpc9BDwNYapU96vj9RXLqxji32Sv6wVF4nJYU,13802
17
30
  test/test_analyzer.py,sha256=WhEYSI1atRee2Gqd9-Edw6F-tdRS9DBqS0D0aqK6MKc,21093
18
31
  test/test_changes_analyzer.py,sha256=l1hspCFz-sF8gqOilJvntUDuGckwhYsVtzvSRB13ZWw,5085
19
32
  test/test_cli.py,sha256=rtdKzanDRJT_F92jKkCQFdhvlfwVJxfXKO8Hrbn-mIg,13180
33
+ test/test_cmd_injection.py,sha256=sG5Ryq18kPKhawB4ZxbGYeXPcCHagFTcWultNoX9XxY,1160
20
34
  test/test_codemods.py,sha256=Tbp9jE95HDl77EenTmyTtB1Sc3L8fwY9xiMNVDN5lxo,4522
21
35
  test/test_constants.py,sha256=pMuDy0UpC81zENMDCeK6Bqmm3BR_HHZQSlMG-9TgOm0,12602
36
+ test/test_dangerous.py,sha256=aaksXFYrNb0gSCdbrYmL77EMmQC6F395NKzUmV8opYA,3427
22
37
  test/test_framework_aware.py,sha256=G9va1dEQ31wsvd4X7ROf_1YhhAQG5CogB7v0hYCojQ8,8802
23
38
  test/test_integration.py,sha256=bNKGUe-w0xEZEdnoQNHbssvKMGs9u9fmFQTOz1lX9_k,12398
24
39
  test/test_new_behaviours.py,sha256=vAON8rR09RXawZT1knYtZHseyRcECKz5bdOspJoMjUA,1472
25
- test/test_secrets.py,sha256=rjC-iQD9s8U296PyHP0vMEjC6CC9mJwKiFj7Sm-xsuo,5711
40
+ test/test_path_traversal.py,sha256=CI49yJwLqHIHwqrPkJwjEBz7pY7dy5B8A62H8qu_YGk,1098
41
+ test/test_secrets.py,sha256=cxbe8zH6lv5aKgazCW01q-mX5F2dRsiH6N2lDXb7ueY,6010
26
42
  test/test_skylos.py,sha256=kz77STrS4k3Eez5RDYwGxOg2WH3e7zNZPUYEaTLbGTs,15608
43
+ test/test_sql_injection.py,sha256=BvwGTKgshFWWF8on1Qoa2_wKKgF5noBfpq-dtnx6DsM,1634
44
+ test/test_ssrf.py,sha256=ob3D36o_Me0TjOcVeFUwHBAVERbsgdBqtuW6F2smgGM,1445
27
45
  test/test_test_aware.py,sha256=VmbR_MQY0m941CAxxux8OxJHIr7l8crfWRouSeBMhIo,9390
28
46
  test/test_visitor.py,sha256=xAbGv-XaozKm_0WJJhr0hMb6mLaJcbPz57G9-SWkxFU,22764
29
47
  test/sample_repo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -33,8 +51,8 @@ test/sample_repo/sample_repo/commands.py,sha256=b6gQ9YDabt2yyfqGbOpLo0osF7wya8O4
33
51
  test/sample_repo/sample_repo/models.py,sha256=xXIg3pToEZwKuUCmKX2vTlCF_VeFA0yZlvlBVPIy5Qw,3320
34
52
  test/sample_repo/sample_repo/routes.py,sha256=8yITrt55BwS01G7nWdESdx8LuxmReqop1zrGUKPeLi8,2475
35
53
  test/sample_repo/sample_repo/utils.py,sha256=S56hEYh8wkzwsD260MvQcmUFOkw2EjFU27nMLFE6G2k,1103
36
- skylos-2.2.3.dist-info/METADATA,sha256=ug19tHc6winuwwnlmWqv87j18KfrnAKs14KvFRA-ivo,314
37
- skylos-2.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
- skylos-2.2.3.dist-info/entry_points.txt,sha256=zzRpN2ByznlQoLeuLolS_TFNYSQxUGBL1EXQsAd6bIA,43
39
- skylos-2.2.3.dist-info/top_level.txt,sha256=f8GA_7KwfaEopPMP8-EXDQXaqd4IbsOQPakZy01LkdQ,12
40
- skylos-2.2.3.dist-info/RECORD,,
54
+ skylos-2.4.0.dist-info/METADATA,sha256=_zgyV0RiF1UQMnVYxfWzNzW5fa-vt3y1kNIu0xD32eU,314
55
+ skylos-2.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
56
+ skylos-2.4.0.dist-info/entry_points.txt,sha256=zzRpN2ByznlQoLeuLolS_TFNYSQxUGBL1EXQsAd6bIA,43
57
+ skylos-2.4.0.dist-info/top_level.txt,sha256=f8GA_7KwfaEopPMP8-EXDQXaqd4IbsOQPakZy01LkdQ,12
58
+ skylos-2.4.0.dist-info/RECORD,,
@@ -0,0 +1,41 @@
1
+ from pathlib import Path
2
+ from skylos.rules.danger import scan_ctx
3
+
4
+ def _write(tmp_path: Path, name, code):
5
+ p = tmp_path / name
6
+ p.write_text(code, encoding="utf-8")
7
+ return p
8
+
9
+ def _rule_ids(findings):
10
+ return {f["rule_id"] for f in findings}
11
+
12
+ def _scan_one(tmp_path: Path, name, code):
13
+ file_path = _write(tmp_path, name, code)
14
+ return scan_ctx(tmp_path, [file_path])
15
+
16
+ def test_os_system_tainted_flags(tmp_path):
17
+ code = (
18
+ "import os\n"
19
+ "def f(x):\n"
20
+ " os.system('echo ' + x)\n"
21
+ )
22
+ out = _scan_one(tmp_path, "cmd_os.py", code)
23
+ assert 'SKY-D212' in _rule_ids(out)
24
+
25
+ def test_subprocess_shell_true_tainted_flags(tmp_path):
26
+ code = (
27
+ "import subprocess\n"
28
+ "def f(cmd):\n"
29
+ " subprocess.run('sh -c ' + cmd, shell=True)\n"
30
+ )
31
+ out = _scan_one(tmp_path, "cmd_subp.py", code)
32
+ assert 'SKY-D212' in _rule_ids(out)
33
+
34
+ def test_os_system_constant_does_not_trigger_D212(tmp_path):
35
+ code = (
36
+ "import os\n"
37
+ "def f():\n"
38
+ " os.system('echo hi')\n"
39
+ )
40
+ out = _scan_one(tmp_path, "cmd_const.py", code)
41
+ assert 'SKY-D212' not in _rule_ids(out)
test/test_dangerous.py ADDED
@@ -0,0 +1,101 @@
1
+ from pathlib import Path
2
+ from skylos.rules.danger import scan_ctx
3
+
4
+ def _write(tmp_path: Path, name, code):
5
+ p = tmp_path / name
6
+ p.write_text(code, encoding="utf-8")
7
+ return p
8
+
9
+ def _rule_ids(findings):
10
+ rule_ids = set()
11
+ for f in findings:
12
+ rule_ids.add(f["rule_id"])
13
+ return rule_ids
14
+
15
+ def _scan_one(tmp_path: Path, name, code):
16
+ file_path = _write(tmp_path, name, code)
17
+ return scan_ctx(tmp_path, [file_path])
18
+
19
+ def test_eval(tmp_path):
20
+ out = _scan_one(tmp_path, "a_eval.py", 'eval("1+1")\n')
21
+ assert "SKY-D201" in _rule_ids(out)
22
+
23
+ def test_exec(tmp_path):
24
+ out = _scan_one(tmp_path, "a_exec.py", 'exec("print(1)")\n')
25
+ assert "SKY-D202" in _rule_ids(out)
26
+
27
+ def test_os_system(tmp_path):
28
+ out = _scan_one(tmp_path, "a_os.py", "import os\nos.system('echo hi')\n")
29
+ assert "SKY-D203" in _rule_ids(out)
30
+
31
+ def test_pickle_loads(tmp_path):
32
+ out = _scan_one(tmp_path, "a_pickle.py", "import pickle\npickle.loads(b'\\x80\\x04K\\x01.')\n")
33
+ assert "SKY-D205" in _rule_ids(out)
34
+
35
+ def test_yaml_load_without_safeloader(tmp_path):
36
+ out = _scan_one(tmp_path, "a_yaml.py", "import yaml\nyaml.load('a: 1')\n")
37
+ assert "SKY-D206" in _rule_ids(out)
38
+
39
+ def test_md5_sha1(tmp_path):
40
+ out = _scan_one(tmp_path, "a_hashes.py", "import hashlib\nhashlib.md5(b'd')\nhashlib.sha1(b'd')\n")
41
+ ids = _rule_ids(out)
42
+ assert "SKY-D207" in ids
43
+ assert "SKY-D208" in ids
44
+
45
+ def test_subprocess_shell_true(tmp_path):
46
+ out = _scan_one(tmp_path, "a_subproc.py", "import subprocess\nsubprocess.run('echo hi', shell=True)\n")
47
+ assert "SKY-D209" in _rule_ids(out)
48
+
49
+ def test_requests_verify_false(tmp_path):
50
+ out = _scan_one(tmp_path, "a_requests.py", "import requests\nrequests.get('https://x', verify=False)\n")
51
+ assert "SKY-D210" in _rule_ids(out)
52
+
53
+ def test_yaml_safe_loader_does_not_trigger(tmp_path):
54
+ code = (
55
+ "import yaml\n"
56
+ "from yaml import SafeLoader\n"
57
+ "yaml.load('a: 1', Loader=SafeLoader)\n"
58
+ )
59
+ out = _scan_one(tmp_path, "b_yaml_safe.py", code)
60
+ assert "SKY-D206" not in _rule_ids(out)
61
+
62
+ def test_subprocess_without_shell_true_is_ok(tmp_path):
63
+ code = "import subprocess\nsubprocess.run(['echo','hi'])\n"
64
+ out = _scan_one(tmp_path, "b_subproc_ok.py", code)
65
+ assert "SKY-D209" not in _rule_ids(out)
66
+
67
+ def test_requests_default_verify_true_is_ok(tmp_path):
68
+ code = "import requests\nrequests.get('https://example.com')\n"
69
+ out = _scan_one(tmp_path, "b_requests_ok.py", code)
70
+ assert "SKY-D210" not in _rule_ids(out)
71
+
72
+ def test_sql_execute_interpolated_flags(tmp_path):
73
+ code = """
74
+ def f(cur, name):
75
+ # f-string interpolation -> should flag SKY-D211
76
+ cur.execute(f"SELECT * FROM users WHERE name = '{name}'")
77
+ """
78
+ out = _scan_one(tmp_path, "sql_interp.py", code)
79
+ assert "SKY-D211" in _rule_ids(out)
80
+
81
+
82
+ def test_sql_execute_parameterized_ok(tmp_path):
83
+ code = """
84
+ def f(cur, name):
85
+ cur.execute("SELECT * FROM users WHERE name = %s", (name,))
86
+ """
87
+ out = _scan_one(tmp_path, "sql_param_ok.py", code)
88
+ assert "SKY-D211" not in _rule_ids(out)
89
+
90
+
91
+ def test_sql_executescript_or_executemany_interpolated_flags(tmp_path):
92
+ code = """
93
+ def g(cur, tbl):
94
+ cur.executescript("CREATE TABLE " + tbl)
95
+
96
+ def h(cur, values):
97
+ cur.executemany("INSERT INTO t (a,b) VALUES (" + values + ")", [])
98
+ """
99
+ out = _scan_one(tmp_path, "sql_execscripts.py", code)
100
+ ids = _rule_ids(out)
101
+ assert "SKY-D211" in ids
@@ -0,0 +1,40 @@
1
+ from pathlib import Path
2
+ from skylos.rules.danger import scan_ctx
3
+
4
+ def _write(tmp_path: Path, name, code):
5
+ p = tmp_path / name
6
+ p.write_text(code, encoding="utf-8")
7
+ return p
8
+
9
+ def _rule_ids(findings):
10
+ return {f["rule_id"] for f in findings}
11
+
12
+ def _scan_one(tmp_path: Path, name, code):
13
+ file_path = _write(tmp_path, name, code)
14
+ return scan_ctx(tmp_path, [file_path])
15
+
16
+ def test_open_tainted_flags(tmp_path):
17
+ code = (
18
+ "def f(p):\n"
19
+ " with open(p, 'r', encoding='utf-8', errors='ignore') as fh:\n"
20
+ " fh.read()\n"
21
+ )
22
+ out = _scan_one(tmp_path, "pt_open.py", code)
23
+ assert 'SKY-D215' in _rule_ids(out)
24
+
25
+ def test_os_remove_tainted_flags(tmp_path):
26
+ code = (
27
+ "import os\n"
28
+ "def f(p):\n"
29
+ " os.remove(p)\n"
30
+ )
31
+ out = _scan_one(tmp_path, "pt_os.py", code)
32
+ assert 'SKY-D215' in _rule_ids(out)
33
+
34
+ def test_open_constant_ok(tmp_path):
35
+ code = (
36
+ "def f():\n"
37
+ " open('README.md', 'r')\n"
38
+ )
39
+ out = _scan_one(tmp_path, "pt_ok.py", code)
40
+ assert 'SKY-D215' not in _rule_ids(out)
test/test_secrets.py CHANGED
@@ -77,16 +77,6 @@ def test_aws_secret_access_key_special_case():
77
77
  assert "entropy" in hit and isinstance(hit["entropy"], float)
78
78
  assert ELLIPSIS in hit["preview"]
79
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
80
  def test_ignore_directive_suppresses_matches():
91
81
  src = 'GITHUB_TOKEN = "ghp_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" # skylos: ignore[SKY-S101]\n'
92
82
  findings = list(scan_ctx(_ctx_from_source(src)))
@@ -177,3 +167,27 @@ def test_safe_hints_suppress_detection():
177
167
  safe_line = 'EXAMPLE_TOKEN = "sk_test_this_is_example_value_not_real_123456"\n'
178
168
  out = list(scan_ctx(_ctx_from_source(safe_line)))
179
169
  assert out == []
170
+
171
+ def test_generic_is_suppressed_in_test_paths():
172
+ src = 'X = "o2uV7Ew1kZ9Q3nR8sT5yU6pX4cJ2mL7a"\n'
173
+ findings = list(scan_ctx(_ctx_from_source(src, rel="tests/unit/test_secrets.py")))
174
+
175
+ generic_findings = []
176
+ for f in findings:
177
+ if f["provider"] == "generic":
178
+ generic_findings.append(f)
179
+
180
+ assert len(generic_findings) == 0
181
+
182
+ def test_normal_strings_ignored():
183
+ src = 'X = "config_path"\n'
184
+ ctx = _ctx_from_source(src)
185
+ findings = list(scan_ctx(ctx))
186
+
187
+ generic_findings = []
188
+ for f in findings:
189
+ if f["provider"] == "generic":
190
+ generic_findings.append(f)
191
+
192
+ assert len(generic_findings) == 0
193
+
@@ -0,0 +1,54 @@
1
+ from pathlib import Path
2
+ from skylos.rules.danger import scan_ctx
3
+
4
+ def _write(tmp_path: Path, name, code):
5
+ p = tmp_path / name
6
+ p.write_text(code, encoding="utf-8")
7
+ return p
8
+
9
+ def _rule_ids(findings):
10
+ return {f["rule_id"] for f in findings}
11
+
12
+ def _scan_one(tmp_path: Path, name, code):
13
+ file_path = _write(tmp_path, name, code)
14
+ return scan_ctx(tmp_path, [file_path])
15
+
16
+ def test_sqlalchemy_text_tainted_flags(tmp_path):
17
+ code = (
18
+ "import sqlalchemy as sa\n"
19
+ "def f(ip):\n"
20
+ " sa.text('DELETE FROM logs WHERE ip=' + ip)\n"
21
+ )
22
+ out = _scan_one(tmp_path, "raw_sa.py", code)
23
+ assert 'SKY-D217' in _rule_ids(out)
24
+
25
+ def test_pandas_read_sql_tainted_flags(tmp_path):
26
+ code = (
27
+ "import pandas as pd\n"
28
+ "def f(conn, name):\n"
29
+ " pd.read_sql(f\"SELECT * FROM users WHERE name='{name}'\", conn)\n"
30
+ )
31
+ out = _scan_one(tmp_path, "raw_pd.py", code)
32
+ assert 'SKY-D217' in _rule_ids(out)
33
+
34
+ def test_django_objects_raw_tainted_flags(tmp_path):
35
+ code = (
36
+ "class _O:\n"
37
+ " def raw(self, *a, **k):\n"
38
+ " return []\n"
39
+ "class User:\n"
40
+ " objects = _O()\n"
41
+ "def f(u):\n"
42
+ " User.objects.raw('SELECT * FROM auth_user WHERE username=' + u)\n"
43
+ )
44
+ out = _scan_one(tmp_path, "raw_dj.py", code)
45
+ assert 'SKY-D217' in _rule_ids(out)
46
+
47
+ def test_raw_constant_ok(tmp_path):
48
+ code = (
49
+ "import pandas as pd\n"
50
+ "def f(conn):\n"
51
+ " pd.read_sql('SELECT 1 AS x', conn)\n"
52
+ )
53
+ out = _scan_one(tmp_path, "raw_ok.py", code)
54
+ assert 'SKY-D217' not in _rule_ids(out)
test/test_ssrf.py ADDED
@@ -0,0 +1,51 @@
1
+ from pathlib import Path
2
+ from skylos.rules.danger import scan_ctx
3
+
4
+ def _write(tmp_path: Path, name, code):
5
+ p = tmp_path / name
6
+ p.write_text(code, encoding="utf-8")
7
+ return p
8
+
9
+ def _rule_ids(findings):
10
+ return {f["rule_id"] for f in findings}
11
+
12
+ def _scan_one(tmp_path: Path, name, code):
13
+ file_path = _write(tmp_path, name, code)
14
+ return scan_ctx(tmp_path, [file_path])
15
+
16
+ def test_requests_tainted_url_flags(tmp_path):
17
+ code = (
18
+ "import requests\n"
19
+ "def f():\n"
20
+ " u = input()\n"
21
+ " requests.get(u)\n"
22
+ )
23
+ out = _scan_one(tmp_path, "ssrf_req.py", code)
24
+ assert 'SKY-D216' in _rule_ids(out)
25
+
26
+ def test_httpx_tainted_url_flags(tmp_path):
27
+ code = (
28
+ "import httpx\n"
29
+ "def f(url):\n"
30
+ " httpx.post('http://' + url)\n"
31
+ )
32
+ out = _scan_one(tmp_path, "ssrf_httpx.py", code)
33
+ assert 'SKY-D216' in _rule_ids(out)
34
+
35
+ def test_urllib_urlopen_tainted_url_flags(tmp_path):
36
+ code = (
37
+ "import urllib.request as u\n"
38
+ "def f(x):\n"
39
+ " u.urlopen(f'http://{x}')\n"
40
+ )
41
+ out = _scan_one(tmp_path, "ssrf_urlopen.py", code)
42
+ assert 'SKY-D216' in _rule_ids(out)
43
+
44
+ def test_requests_constant_url_ok(tmp_path):
45
+ code = (
46
+ "import requests\n"
47
+ "def f():\n"
48
+ " requests.get('https://example.com', timeout=3)\n"
49
+ )
50
+ out = _scan_one(tmp_path, "ssrf_ok.py", code)
51
+ assert 'SKY-D216' not in _rule_ids(out)
File without changes