skylos 2.2.4__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 +107 -76
- skylos/cli.py +156 -8
- 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-2.2.4.dist-info → skylos-2.4.0.dist-info}/METADATA +1 -1
- {skylos-2.2.4.dist-info → skylos-2.4.0.dist-info}/RECORD +26 -10
- test/test_cmd_injection.py +41 -0
- test/test_dangerous.py +32 -1
- test/test_path_traversal.py +40 -0
- test/test_sql_injection.py +54 -0
- test/test_ssrf.py +51 -0
- skylos/rules/dangerous.py +0 -135
- {skylos-2.2.4.dist-info → skylos-2.4.0.dist-info}/WHEEL +0 -0
- {skylos-2.2.4.dist-info → skylos-2.4.0.dist-info}/entry_points.txt +0 -0
- {skylos-2.2.4.dist-info → skylos-2.4.0.dist-info}/top_level.txt +0 -0
|
@@ -1,13 +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/dangerous.py,sha256=a1P2wpjxxAHOG_pa83xSKDOnb1HFGPlHzATtkgQtd8Y,4111
|
|
10
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
|
|
11
23
|
skylos/visitors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
24
|
skylos/visitors/framework_aware.py,sha256=eX4oU0jQwDWejkkW4kjRNctre27sVLHK1CTDDiqPqRw,13054
|
|
13
25
|
skylos/visitors/test_aware.py,sha256=kxYoMG2m02kbMlxtFOM-MWJO8qqxHviP9HgAMbKRfvU,2304
|
|
@@ -18,14 +30,18 @@ test/diagnostics.py,sha256=ExuFOCVpc9BDwNYapU96vj9RXLqxji32Sv6wVF4nJYU,13802
|
|
|
18
30
|
test/test_analyzer.py,sha256=WhEYSI1atRee2Gqd9-Edw6F-tdRS9DBqS0D0aqK6MKc,21093
|
|
19
31
|
test/test_changes_analyzer.py,sha256=l1hspCFz-sF8gqOilJvntUDuGckwhYsVtzvSRB13ZWw,5085
|
|
20
32
|
test/test_cli.py,sha256=rtdKzanDRJT_F92jKkCQFdhvlfwVJxfXKO8Hrbn-mIg,13180
|
|
33
|
+
test/test_cmd_injection.py,sha256=sG5Ryq18kPKhawB4ZxbGYeXPcCHagFTcWultNoX9XxY,1160
|
|
21
34
|
test/test_codemods.py,sha256=Tbp9jE95HDl77EenTmyTtB1Sc3L8fwY9xiMNVDN5lxo,4522
|
|
22
35
|
test/test_constants.py,sha256=pMuDy0UpC81zENMDCeK6Bqmm3BR_HHZQSlMG-9TgOm0,12602
|
|
23
|
-
test/test_dangerous.py,sha256=
|
|
36
|
+
test/test_dangerous.py,sha256=aaksXFYrNb0gSCdbrYmL77EMmQC6F395NKzUmV8opYA,3427
|
|
24
37
|
test/test_framework_aware.py,sha256=G9va1dEQ31wsvd4X7ROf_1YhhAQG5CogB7v0hYCojQ8,8802
|
|
25
38
|
test/test_integration.py,sha256=bNKGUe-w0xEZEdnoQNHbssvKMGs9u9fmFQTOz1lX9_k,12398
|
|
26
39
|
test/test_new_behaviours.py,sha256=vAON8rR09RXawZT1knYtZHseyRcECKz5bdOspJoMjUA,1472
|
|
40
|
+
test/test_path_traversal.py,sha256=CI49yJwLqHIHwqrPkJwjEBz7pY7dy5B8A62H8qu_YGk,1098
|
|
27
41
|
test/test_secrets.py,sha256=cxbe8zH6lv5aKgazCW01q-mX5F2dRsiH6N2lDXb7ueY,6010
|
|
28
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
|
|
29
45
|
test/test_test_aware.py,sha256=VmbR_MQY0m941CAxxux8OxJHIr7l8crfWRouSeBMhIo,9390
|
|
30
46
|
test/test_visitor.py,sha256=xAbGv-XaozKm_0WJJhr0hMb6mLaJcbPz57G9-SWkxFU,22764
|
|
31
47
|
test/sample_repo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -35,8 +51,8 @@ test/sample_repo/sample_repo/commands.py,sha256=b6gQ9YDabt2yyfqGbOpLo0osF7wya8O4
|
|
|
35
51
|
test/sample_repo/sample_repo/models.py,sha256=xXIg3pToEZwKuUCmKX2vTlCF_VeFA0yZlvlBVPIy5Qw,3320
|
|
36
52
|
test/sample_repo/sample_repo/routes.py,sha256=8yITrt55BwS01G7nWdESdx8LuxmReqop1zrGUKPeLi8,2475
|
|
37
53
|
test/sample_repo/sample_repo/utils.py,sha256=S56hEYh8wkzwsD260MvQcmUFOkw2EjFU27nMLFE6G2k,1103
|
|
38
|
-
skylos-2.
|
|
39
|
-
skylos-2.
|
|
40
|
-
skylos-2.
|
|
41
|
-
skylos-2.
|
|
42
|
-
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
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
|
-
from skylos.rules.
|
|
2
|
+
from skylos.rules.danger import scan_ctx
|
|
3
3
|
|
|
4
4
|
def _write(tmp_path: Path, name, code):
|
|
5
5
|
p = tmp_path / name
|
|
@@ -68,3 +68,34 @@ def test_requests_default_verify_true_is_ok(tmp_path):
|
|
|
68
68
|
code = "import requests\nrequests.get('https://example.com')\n"
|
|
69
69
|
out = _scan_one(tmp_path, "b_requests_ok.py", code)
|
|
70
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)
|
|
@@ -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)
|
skylos/rules/dangerous.py
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
import ast
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
ALLOWED_SUFFIXES = (".py", ".pyi", ".pyw")
|
|
6
|
-
|
|
7
|
-
## will expand this list later with more rules
|
|
8
|
-
DANGEROUS_CALLS = {
|
|
9
|
-
"eval": ("SKY-D201", "HIGH", "Use of eval()"),
|
|
10
|
-
"exec": ("SKY-D202", "HIGH", "Use of exec()"),
|
|
11
|
-
"os.system": ("SKY-D203", "MEDIUM", "Use of os.system"),
|
|
12
|
-
"pickle.load": ("SKY-D204", "CRITICAL", "Untrusted deserialization via pickle.load"),
|
|
13
|
-
"pickle.loads": ("SKY-D205", "CRITICAL", "Untrusted deserialization via pickle.loads"),
|
|
14
|
-
"yaml.load": ("SKY-D206", "HIGH", "yaml.load without SafeLoader"),
|
|
15
|
-
"hashlib.md5": ("SKY-D207", "MEDIUM", "Weak hash (MD5)"),
|
|
16
|
-
"hashlib.sha1": ("SKY-D208", "MEDIUM", "Weak hash (SHA1)"),
|
|
17
|
-
## this is for arguments like process
|
|
18
|
-
"subprocess.*": ("SKY-D209", "HIGH", "subprocess.* with shell=True",
|
|
19
|
-
{"kw_equals": {"shell": True}}),
|
|
20
|
-
|
|
21
|
-
"requests.*": ("SKY-D210", "HIGH", "requests call with verify=False",
|
|
22
|
-
{"kw_equals": {"verify": False}}),
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
def _matches_rule(name, rule_key):
|
|
26
|
-
if not name:
|
|
27
|
-
return False
|
|
28
|
-
if rule_key.endswith(".*"):
|
|
29
|
-
return name.startswith(rule_key[:-2] + ".")
|
|
30
|
-
return name == rule_key
|
|
31
|
-
|
|
32
|
-
def _kw_equals(node: ast.Call, requirements):
|
|
33
|
-
if not requirements:
|
|
34
|
-
return True
|
|
35
|
-
kw_map = {}
|
|
36
|
-
keywords = node.keywords or []
|
|
37
|
-
for kw in keywords:
|
|
38
|
-
if kw.arg:
|
|
39
|
-
kw_map[kw.arg] = kw.value
|
|
40
|
-
|
|
41
|
-
for key, expected in requirements.items():
|
|
42
|
-
val = kw_map.get(key)
|
|
43
|
-
if not isinstance(val, ast.Constant):
|
|
44
|
-
return False
|
|
45
|
-
if val.value is not expected:
|
|
46
|
-
return False
|
|
47
|
-
return True
|
|
48
|
-
|
|
49
|
-
def qualified_name_from_call(node: ast.Call):
|
|
50
|
-
f = node.func
|
|
51
|
-
parts = []
|
|
52
|
-
while isinstance(f, ast.Attribute):
|
|
53
|
-
parts.append(f.attr)
|
|
54
|
-
f = f.value
|
|
55
|
-
if isinstance(f, ast.Name):
|
|
56
|
-
parts.append(f.id)
|
|
57
|
-
parts.reverse()
|
|
58
|
-
return ".".join(parts)
|
|
59
|
-
if isinstance(f, ast.Name):
|
|
60
|
-
return f.id
|
|
61
|
-
return None
|
|
62
|
-
|
|
63
|
-
def _yaml_load_without_safeloader(node: ast.Call):
|
|
64
|
-
name = qualified_name_from_call(node)
|
|
65
|
-
if name != "yaml.load":
|
|
66
|
-
return False
|
|
67
|
-
|
|
68
|
-
for kw in node.keywords or []:
|
|
69
|
-
if kw.arg == "Loader":
|
|
70
|
-
try:
|
|
71
|
-
text = ast.unparse(kw.value)
|
|
72
|
-
return "SafeLoader" not in text
|
|
73
|
-
except Exception:
|
|
74
|
-
return True
|
|
75
|
-
return True
|
|
76
|
-
|
|
77
|
-
def _add_finding(findings,
|
|
78
|
-
file_path: Path,
|
|
79
|
-
node: ast.AST,
|
|
80
|
-
rule_id,
|
|
81
|
-
severity,
|
|
82
|
-
message):
|
|
83
|
-
findings.append({
|
|
84
|
-
"rule_id": rule_id,
|
|
85
|
-
"severity": severity,
|
|
86
|
-
"message": message,
|
|
87
|
-
"file": str(file_path),
|
|
88
|
-
"line": getattr(node, "lineno", 1),
|
|
89
|
-
"col": getattr(node, "col_offset", 0),
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
def scan_ctx(root, files):
|
|
93
|
-
findings = []
|
|
94
|
-
|
|
95
|
-
for file_path in files:
|
|
96
|
-
if file_path.suffix.lower() not in ALLOWED_SUFFIXES:
|
|
97
|
-
continue
|
|
98
|
-
|
|
99
|
-
try:
|
|
100
|
-
src = file_path.read_text(encoding="utf-8", errors="ignore")
|
|
101
|
-
tree = ast.parse(src)
|
|
102
|
-
except Exception:
|
|
103
|
-
continue
|
|
104
|
-
|
|
105
|
-
for node in ast.walk(tree):
|
|
106
|
-
if not isinstance(node, ast.Call):
|
|
107
|
-
continue
|
|
108
|
-
|
|
109
|
-
name = qualified_name_from_call(node)
|
|
110
|
-
if not name:
|
|
111
|
-
continue
|
|
112
|
-
|
|
113
|
-
for rule_key, tup in DANGEROUS_CALLS.items():
|
|
114
|
-
rule_id, severity, message, *rest = tup
|
|
115
|
-
|
|
116
|
-
if rest:
|
|
117
|
-
opts = rest[0]
|
|
118
|
-
else:
|
|
119
|
-
opts = None
|
|
120
|
-
|
|
121
|
-
if not _matches_rule(name, rule_key):
|
|
122
|
-
continue
|
|
123
|
-
|
|
124
|
-
if rule_key == "yaml.load":
|
|
125
|
-
if not _yaml_load_without_safeloader(node):
|
|
126
|
-
continue
|
|
127
|
-
|
|
128
|
-
if opts and "kw_equals" in opts:
|
|
129
|
-
if not _kw_equals(node, opts["kw_equals"]):
|
|
130
|
-
continue
|
|
131
|
-
|
|
132
|
-
_add_finding(findings, file_path, node, rule_id, severity, message)
|
|
133
|
-
break
|
|
134
|
-
|
|
135
|
-
return findings
|
|
File without changes
|
|
File without changes
|
|
File without changes
|