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.

@@ -1,13 +1,25 @@
1
- skylos/__init__.py,sha256=YwPfQwovq4tgmZty1bPKmaNXjqgsmv6wbi94D2oEo6Y,229
2
- skylos/analyzer.py,sha256=iiymCNg3LphPSHhR3TYLmObR2vlGC99wKligDtHHzDE,17449
3
- skylos/cli.py,sha256=oEZHS8ogV8pZloj_EIk4qzPVbJS7QenHKJDIp7q2OGg,19882
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=zFc2Fi43T824yWY-nKziP1YH_Z4on5rXwxsvgYQ6EN4,2523
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.2.4.dist-info/METADATA,sha256=S5mGNVK7yVnSeqxS_5pr1PEXqaUrCcdu3NDZEqDHdYo,314
39
- skylos-2.2.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
40
- skylos-2.2.4.dist-info/entry_points.txt,sha256=zzRpN2ByznlQoLeuLolS_TFNYSQxUGBL1EXQsAd6bIA,43
41
- skylos-2.2.4.dist-info/top_level.txt,sha256=f8GA_7KwfaEopPMP8-EXDQXaqd4IbsOQPakZy01LkdQ,12
42
- skylos-2.2.4.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 CHANGED
@@ -1,5 +1,5 @@
1
1
  from pathlib import Path
2
- from skylos.rules.dangerous import scan_ctx
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