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/__init__.py +1 -1
- skylos/analyzer.py +119 -67
- skylos/cli.py +172 -10
- skylos/rules/danger/__init__.py +0 -0
- skylos/rules/danger/danger.py +141 -0
- skylos/rules/danger/danger_cmd/__init__.py +0 -0
- skylos/rules/danger/danger_cmd/cmd_flow.py +208 -0
- skylos/rules/danger/danger_fs/__init__.py +0 -0
- skylos/rules/danger/danger_fs/path_flow.py +188 -0
- skylos/rules/danger/danger_net/__init__.py +0 -0
- skylos/rules/danger/danger_net/ssrf_flow.py +198 -0
- skylos/rules/danger/danger_sql/__init__.py +0 -0
- skylos/rules/danger/danger_sql/sql_flow.py +175 -0
- skylos/rules/danger/danger_sql/sql_raw_flow.py +202 -0
- skylos/rules/danger/danger_web/__init__.py +0 -0
- skylos/rules/danger/danger_web/xss_flow.py +279 -0
- skylos/rules/secrets.py +34 -5
- {skylos-2.2.3.dist-info → skylos-2.4.0.dist-info}/METADATA +1 -1
- {skylos-2.2.3.dist-info → skylos-2.4.0.dist-info}/RECORD +28 -10
- test/test_cmd_injection.py +41 -0
- test/test_dangerous.py +101 -0
- test/test_path_traversal.py +40 -0
- test/test_secrets.py +24 -10
- test/test_sql_injection.py +54 -0
- test/test_ssrf.py +51 -0
- {skylos-2.2.3.dist-info → skylos-2.4.0.dist-info}/WHEEL +0 -0
- {skylos-2.2.3.dist-info → skylos-2.4.0.dist-info}/entry_points.txt +0 -0
- {skylos-2.2.3.dist-info → skylos-2.4.0.dist-info}/top_level.txt +0 -0
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
|
-
)
|
|
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.
|
|
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
|
-
|
|
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,12 +1,25 @@
|
|
|
1
|
-
skylos/__init__.py,sha256=
|
|
2
|
-
skylos/analyzer.py,sha256=
|
|
3
|
-
skylos/cli.py,sha256=
|
|
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=
|
|
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/
|
|
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.
|
|
37
|
-
skylos-2.
|
|
38
|
-
skylos-2.
|
|
39
|
-
skylos-2.
|
|
40
|
-
skylos-2.
|
|
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
|
|
File without changes
|
|
File without changes
|